@songhanshi
2021-05-10T08:42:23.000000Z
字数 303941
阅读 1008
1234
https://www.nowcoder.com/discuss/594676?source_id=discuss_experience_nctrack&channel=-1
负载均衡--https://blog.csdn.net/My_Way666/article/details/91433816
zk -- https://www.jianshu.com/p/30f3c0ce2c5b
加一个
是什么:
-程序运行过程中可以中断程序指令正常执行的事件
2种分类:
-Error、Exception
-父类都是java.lang.Throwable
-Error 系统错误 rd无法处理
-Exception rd可捕获异常
可查异常和不可查异常
生产中:
-空指针
-下标越界
应用:
Hibernate怎样知道他要存的某个对象都有什么属性呢?这些属性都是什么类型呢?
应用
1 反射原理
1)反射的概念
--概念:运行时,而非编译
2)反射机制的作用
--在运行时判断任意一个对象所属的类
--在运行时获取类的对象
--在运行时访问Java对象的属性,方法,构造方法等
3)实现依赖:reflect&Class
1> java.lang.reflect类库里面主要的类
File:表示类中的成员变量
Method:表示类中的方法
Constructor:表示类的构造方法
Array:该类提供了创建数组和访问数组元素的静态方法
2> 反射依赖的Class类
--概念:用来表示运行时类型信息的对应类
每个类都有唯一一个与之相对应的Class对象。
Class类为类类型,而Class对象为类类型对象。
--Class类的特点:
Class类也是类的一种,class是关键字。
Class类只有一个私有的构造函数,只有JVM能够创建Class类是实例。(只有一个私有构造函数,无法new)
JVM中只有唯一一个和类相对应的Class对象来描述其类型信息。
--获取Class对象的三种方式:
生产者-消费者
生产者-消费者模式实现批量执行SQL:
将原来直接INSERT数据到数据库的线程作为生产者线程,生产者线程只需将数据添加到任务队列,然后消费者线程负责将任务从任务队列中批量取出并批量执
行。
--示例:创建了5个消费者线程负责批量执行SQL,
5个消费者线程以 while(true){}循环方式批量地获取任务并批量地执行。
需要注意的是,从任务队列中获取批量任务的方法pollTasks()中,
首先是以阻塞方式获取任务队列中的一条任务,而后则是以非阻塞的方式获取任务;
之所以首先采用阻塞方式,是因为如果任务队列中没有任务,这样的方式能够避免无谓的循环。
//任务队列
BlockingQueue<Task> bq=new LinkedBlockingQueue<>(2000);
//启动5个消费者线程
//执行批量任务
void start() {
ExecutorService es=xecutors.newFixedThreadPool(5);
for (int i=0; i<5; i++) {
es.execute(()->{
try {
while (true) {
//获取批量任务
List<Task> ts=pollTasks();
//执行批量任务
execTasks(ts);
}
}catch(Exception e){
e.printStackTrace();
}
});
}
}
//从任务队列中获取批量任务
List<Task> pollTasks() throws InterruptedException{
List<Task> ts=new LinkedList<>();
//阻塞式获取一条任务
Task t = bq.take();
while(t != null){
ts.add(t);
//非阻塞式获取一条任务
t = bq.poll();
}
return ts;
}
//批量执行任务
execTasks(List<Task> ts) {
//省略具体代码无数
}
4.如何实现一个生产者和消费者模型。
3. 消费者重平衡(高可用性、伸缩性)
4. 那些情景下会造成消息漏消费?
5. 如何保证消息不被重复消费(幂等性)
8. 消费者与生产者的工作流程:
2 集合框架内容
集合框架是一个用来代表和操纵集合的统一架构。所有的集合框架都包含如下内容:
• 接口:是代表集合的抽象数据类型。
例如Collection、List、Set、Map等。之所以定义多个接口,是为了以不同的方式操作集合对象
• 实现(类):是集合接口的具体实现。从本质上讲,它们是可重复使用的数据结构,例如:ArrayList、LinkedList、HashSet、HashMap。
• 算法:是实现集合接口的对象里的方法执行的一些有用的计算,例如:搜索和排序。这些算法被称为多态,那是因为相同的方法可以在相似的接口上有着不同的实现。
--除了集合,该框架也定义了几个Map接口和类。Map 里存储的是键/值对。尽管Map不是集合,但是它们完全整合在集合中。
--java集合框架位于java.util包中,所以当使用集合框架的时候需要进行导包。
3 List和Set的区别
List 可重复,顺序存储,数组或者链表
Set 不可重复,无序,使用Map来存储数据
Map 键值对,key到value的映射
4 了解的List和Map
接口 | 实现类 |
---|---|
List | ArrayList、LinkedList |
Set | HashSet、TreeSet、LinkedHashSet |
Map | HashMap、TreeMap、LinkedHashMap、HashTable |
1 实现接口:
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
private static final long serialVersionUID = 8683452581122892189L;
private static final int DEFAULT_CAPACITY = 10;
private static final Object[] EMPTY_ELEMENTDATA = {};
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
transient Object[] elementData;
private int size;
-Cloneable接口
--Object.clone() 浅拷贝调用 调用的对象必须要实现Cloneable接口,CloneNoSupportException异常
-List接口
--定义了实现该接口的类都必须要实现的一组方法 即平时用到的那些方法
2 字段属性
//集合的默认大小
private static final int DEFAULT_CAPACITY = 10;
//空的数组实例
private static final Object[] EMPTY_ELEMENTDATA = {};
//这也是一个空的数组实例,和EMPTY_ELEMENTDATA空数组相比是用于了解添加元素时数组膨胀多少
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
//存储 ArrayList集合的元素,集合的长度即这个数组的长度
//1、当 elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA 时将会清空 ArrayList
//2、当添加第一个元素时,elementData 长度会扩展为 DEFAULT_CAPACITY=10
transient Object[] elementData;
//表示集合的长度
rivate int size;
3 构造函数
-无参:创建初始容量为0的数组
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
-参数->初始大小n:
public ArrayList(int initialCapacity) {
if (initialCapacity > 0) {
this.elementData = new Object[initialCapacity];
} else if (initialCapacity == 0) {
this.elementData = EMPTY_ELEMENTDATA;
} else {
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
}
}
n>0 大小为n的数组
=0 空数组实例 <0 异常
-参数->集合:将集合复制到ArrayList集合中
-new ArrayList()--elementData赋值为DEFAULTCAPACITY_EMPTY_ELEMENTDATA,
new ArrayList(0)--elementData 赋值为 EMPTY_ELEMENTDATA,EMPTY_ELEMENTDATA添加元素会扩容到容量为1,而DEFAULTCAPACITY_EMPTY_ELEMENTDATA扩容之后容量为10
-elementData
transient修饰,JDK不想将整个elementData都序列化或者反序列化,而只是将size和实际存储的元素序列化或反序列化,节省空间和时间。
5 remove
-单个元素的删除:后续元素左移 引用置空GC回收
-for问题:删除某个元素后,list的大小size发生了变化,而索引也在变化,所以会导致你在遍历的时候漏掉某些元素。
如,删除第1个元素后,继续根据索引访问第2个元素时,因为删除的关系后面的元素都往前移动了一位,所以实际访问的是第3个元素。不会报出异常,只会出现漏删的情况;
6 迭代器
-类实现了List接口,而List接口又继承了Collection接口,Collection接口又继承了Iterable接口
-删除
*迭代器初始化过程中会将modCount这个值赋给迭代器的expectedModCount
*Itr的next()迭代的时候,被遍历期间如果内容发生变化,就会改变modCount的值,会校验modCount是否等于expectedModCount
*Itr的remove()移除之后将modCount重新赋值给 expectedModCount(不是ArrayList的remove)
-缺点:迭代器只能向后遍历,不能向前遍历,能够删除元素,但是不能新增元素
9 modCount
-父类AbstractList modCount属性--记录数组修改次数(可查看源码865行)
-ConcurrentModification Exception。即并发修改异常
-快速失败:
在使用迭代器对集合进行迭代的过程中,如果 A 线程正在对集合进行遍历,此时 B 线程对集合进行修改(增加、删除、修改),或者 A 线程在遍历过程中对集合进行修改,都会导致 A 线程抛出 ConcurrentModificationException 异常。
保证modCount在迭代期间不变
-安全失败:
采用安全失败机制的集合容器,在遍历时不是直接在集合内容上访问的,而是先复制原有集合内容,在拷贝的集合上进行遍历。
由于迭代时是对原集合的拷贝进行遍历,所以在遍历过程中对原集合所作的修改并不能被迭代器检测到,故不会抛 ConcurrentModificationException 异常
1 类
// 类
public class LinkedList<E>
extends AbstractSequentialList<E>
implements List<E>, Deque<E>, Cloneable, java.io.Serializable
{
transient int size = 0;
transient Node<E> first;
transient Node<E> last;
....
// Node节点
private static class Node<E> {
E item;
Node<E> next;
Node<E> prev;
Node(Node<E> prev, E element, Node<E> next) {
this.item = element;
this.next = next;
this.prev = prev;
}
}
1 HashMap实现原理
-- JDK1.6&1.7:位桶数组+链表
-- JDK1.8:位桶数组+链表+红黑树
遇到冲突时,HashMap是采用的链地址法/拉链法来解决;
2 HashMap定义:
--散列表,存储键值对(key-value)映射,key和value都可为null
public class HashMap extends AbstractMap
implements Map, Cloneable, Serializable {
• Map接口,键值对映射通用的操作。
key有序,不重复;value无序,可重复
• 承抽象类 AbstractMap,可以不用实现所有的Map接口方法,选择性
• 继承了AbstractMap,实现了Map接口,是否多此一举?LinkedHashSet类似。
3 字段属性
===初始化的数据值===
--serialVersionUID //序列化和反序列化 一致性
--DEFAULT_INITIAL_CAPACITY=1<<4; //默认集合初始容量为16(必须是2的倍数)
--MAXIMUM_CAPACITY = 1 << 30; //最大容量,带参超过此数,默认使用此数
--DEFAULT_LOAD_FACTOR = 0.75f; //默认的填充因子
==下三个是JDK1.8新增,进行红黑树和链表互相转换==
--TREEIFY_THRESHOLD = 8; //当桶(bucket)上的结点数大于8转成红黑树
--UNTREEIFY_THRESHOLD = 6; //桶(bucket)上节点数小于6转链表
--MIN_TREEIFY_CAPACITY = 64; //集合中的容量大于这个值时,桶才能进行树形化,否则桶内元素太多时会扩容,而不是树形化,为了避免进行扩容、树形化选择的冲突,这个值不能小于4*TREEIFY_THRESHOLD
===初始化结构和属性===
--Node[] table; //初始化长度默认是DEFAULT_INITIAL_CAPACITY= 16。长度总是 2的幂
--Set> entrySet; //保存缓存的entrySet()
--size; //集合中存放key-value 的实时数量
--modCount; //记录集合被修改的次数,用于迭代器中的快速失败
--threshold; //调整大小的下一个大小值(容量*加载因子)。capacity*loadFactor。capacity是桶的数量,即table的长度length。当前已占用数组长度的最大值。超过这个数目就重新resize(扩容),扩容后的HashMap容量是之前容量的两倍。
--loadFactor; //加载因子,用来衡量HashMap满的程度;实时装载因子的计算:size/capacity,
loadFactor为什么默认的负载因子0.75
--泊松分布(tips:有点关系)|0.75
--默认负载因子(0.75)在时间和空间成本上提供了很好的折衷。较高的值会降低空间开销,但提高查找成本(体现在大多数的HashMap类的操作,包括get和put)。设置初始大小时,应该考虑预计的entry数在map及其负载系数,并且尽量减少rehash操作的次数。如果初始容量大于最大条目数除以负载因子,rehash操作将不会发生。
--为1:当负载因子是1.0时,也就意味着,只有当数组的值全部填充了,才会发生扩容。这就带来了很大的问题,因为Hash冲突时避免不了的。
后果:当负载因子是1.0的时候,意味着会出现大量的Hash的冲突,底层的红黑树变得异常复杂。对于查询效率极其不利。这种情况就是牺牲了时间来保证空间的利用率。
因此一句话总结就是负载因子过大,虽然空间利用率上去了,但是时间效率降低了。
--为0.5
后果:负载因子是0.5的时候,这也就意味着,当数组中的元素达到了一半就开始扩容,既然填充的元素少了,Hash冲突也会减少,那么底层的链表长度或者是红黑树的高度就会降低。查询效率就会增加。
但是,此时空间利用率就会大大的降低,原本存储1M的数据,现在就意味着需要2M的空间。
总之,就是负载因子太小,虽然时间效率提升了,但是空间利用率降低了。
--选择0.75作为默认的加载因子,完全是时间和空间成本上寻求的一种折衷选择。
--负载因子是0.75的时,空间利用率比较高,而且避免了相当多的Hash冲突,使得底层的链表或者是红黑树的高度比较低,提升了空间效率。
4 构造函数
容量_ab
① 默认无参构造函数
无参构造器,初始化散列表的加载因子为0.75
this.loadFactor = DEFAULT_LOAD_FACTOR;
② 指定初始容量的构造函数
public HashMap(int initialCapacity, float loadFactor) {
->判断初始化容量initialCapacity,<0,异常,>max,赋值max
->判断加载因子,<0,或非数值,异常
->赋值:
this.loadFactor = loadFactor;
this.threshold = tableSizeFor(initialCapacity);
--tableSizeFor(cap)方法:
{
int n = cap - 1;
n |= n >>> 1;
n |= n >>> 2;
n |= n >>> 4;
n |= n >>> 8;
n |= n >>> 16;
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
---这块右移的目的:对于一个数字的二进制,从第一个不为0的位开始,把后面的所有位都设置成1。
---几次无符号右移和按位或运算,把1100 1100 1100转换成了1111 1111 1111 ,再把1111 1111 1111加1,就得到了1 0000 0000 0000,这就是大于1100 1100 1100的第一个2的幂。
//返回大于等于initialCapacity的最小的二次幂数值。>>>操作符表示无符号右移,高位取0。|按位或运算
5 hash算法
HashMap中的hash函数?Hash算法(扰动函数) |3
--散列函数|散列表:
哈希表通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。这个映射函数叫做散列函数,存放记录的数组叫做散列表。
散列函数的存在能够帮助我们更快的确定key和value的映射关系
--HashMap中的哈希算法:确定哈希桶数组索引位置
--三步:
①取hashCode值:key.hashCode()
②高位参与运算:h>>>16
③取模运算:(n-1)&hash
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
// 使用:这一步是在后面添加元素putVal()方法中进行位置的确定
i = (table.length - 1) & hash(key);
--散列函数设计的越好,使得元素分布的越均匀。
hashmap容量为什么是2的幂次
-get()中,(table.length -1)&hash计算出key在table索引位置
-length是2的n次方时,(length-1)&hash等价于length取模,即hash%length,但是&比%具有更高的效率。比如 n % 32 =(32 -1)&n
--为什么?
n-1的二进制永远都是尾端以连续1的形式表示,当(n - 1) & hash会保留hash中后 x 位的 1
0&0 0&1 都为0
1&0 1&1 分布更均匀,减少碰撞几率,加快了查询的效率,空间浪费少。
1|2
6 put
//hash(key)就是上面讲的hash方法,对其进行了第一步和第二步处理
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
/**
*
* @param hash 索引的位置
* @param key 键
* @param value 值
* @param onlyIfAbsent true 表示不要更改现有值
* @param evict false表示table处于创建模式
* @return
*/
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
//如果table为null或者长度为0,则进行初始化
//resize()方法本来是用于扩容,由于初始化没有实际分配空间,这里用该方法进行空间分配,后面会详细讲解该方法
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
//注意:这里用到了前面讲解获得key的hash码的第三步,取模运算,下面的if-else分别是 tab[i] 为null和不为null
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);//tab[i] 为null,直接将新的key-value插入到计算的索引i位置
else {//tab[i] 不为null,表示该位置已经有值了
Node<K,V> e; K k;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;//节点key已经有值了,直接用新值覆盖
//该链是红黑树
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
//该链是链表
else {
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
//链表长度大于8,转换成红黑树
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
//key已经存在直接覆盖value
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;//用作修改和新增快速失败
if (++size > threshold)//超过最大容量,进行扩容
resize();
afterNodeInsertion(evict);
return null;
}
图示
put过程图
-为什么在1.8中链表大于8时会转红黑树?
因为泊松分布,拉链法哈希冲突累积到七个元素后,通过泊松分布计算得到第8个冲突元素出现的概率极低,几乎不可能出现,但只要出现了就树形化提高查询效率(前提是数组长度已经到了64,否则先扩容)
-为什么要用红黑树?而不用平衡二叉树? |2
--Java8之前,链表解决冲突的,产生碰撞,进行get时,两步的时间复杂度是O(1)+O(n)。因此,当碰撞很厉害的时候n很大,O(n)的速度显然是影响速度的。
--Java8中,红黑树替换链表,复杂度就变成了O(1)+O(logn)了,这样在n很大的时候,能够比较理想的解决这个问题
-如果两个键的hashcode相同,你如何获取值对象?
找到bucket位置之后,会调用keys.equals()方法去找到链表中正确的节点,最终找到要找的值对象。
-size:hashMap怎么获取有几个元素,底层实现
public int size() {
return size;
}
// 计算
if (++size > threshold)//超过最大容量,进行扩容 -58
resize(); -59
数组上有5个,某链表上3个,size是多大?
分析第58,59 行代码,调用put()方法添加元素,就会++size(这里有个例外是插入重复key的键值对,不会调用,但是重复key元素不会影响size),所以,上面是 7。
7 resize
//参数 newCapacity 为新数组的大小
void resize(int newCapacity) {
Entry[] oldTable = table;//引用扩容前的 Entry 数组
int oldCapacity = oldTable.length;
if (oldCapacity == MAXIMUM_CAPACITY) {//扩容前的数组大小如果已经达到最大(2^30)了
threshold = Integer.MAX_VALUE;///修改阈值为int的最大值(2^31-1),这样以后就不会扩容了
return;
}
Entry[] newTable = new Entry[newCapacity];//初始化一个新的Entry数组
transfer(newTable, initHashSeedAsNeeded(newCapacity));//将数组元素转移到新数组里面
table = newTable;
threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);//修改阈值
}
void transfer(Entry[] newTable, boolean rehash) {
int newCapacity = newTable.length;
for (Entry<K,V> e : table) {//遍历数组
while(null != e) {
Entry<K,V> next = e.next;
if (rehash) {
e.hash = null == e.key ? 0 : hash(e.key);
}
int i = indexFor(e.hash, newCapacity);//重新计算每个元素在数组中的索引位置
e.next = newTable[i];//标记下一个元素,添加是链表头添加
newTable[i] = e;//将元素放在链上
e = next;//访问下一个 Entry 链上的元素
}
}
}
1.8源码
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length;//原数组如果为null,则长度赋值0
int oldThr = threshold;
int newCap, newThr = 0;
if (oldCap > 0) {//如果原数组长度大于0
// 超过最大值就不再扩充了,就只好随你碰撞去吧
if (oldCap >= MAXIMUM_CAPACITY) {//数组大小如果已经大于等于最大值(2^30)
threshold = Integer.MAX_VALUE;//修改阈值为int的最大值(2^31-1),这样以后就不会扩容了
return oldTab;
}
//原数组长度大于等于初始化长度16,并且原数组长度扩大1倍也小于2^30次方
// 没超过最大值,就扩充为原来的2倍
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // 阀值扩大2倍
}
else if (oldThr > 0) //旧阀值大于0,则将新容量直接等于就阀值
newCap = oldThr;
else {//阀值等于0,oldCap也等于0(集合未进行初始化)
newCap = DEFAULT_INITIAL_CAPACITY;//数组长度初始化为16
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);//阀值等于16*0.75=12
}
//计算新的阀值上限
if (newThr == 0) {
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
threshold = newThr;
@SuppressWarnings({"rawtypes","unchecked"})
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
if (oldTab != null) {
//把每个bucket都移动到新的buckets中
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
if ((e = oldTab[j]) != null) {
oldTab[j] = null;//元数据j位置置为null,便于垃圾回收
if (e.next == null)//数组没有下一个引用(不是链表)
newTab[e.hash & (newCap - 1)] = e;
else if (e instanceof TreeNode)//红黑树
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
else { // preserve order
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
do {
next = e.next;
//原索引
if ((e.hash & oldCap) == 0) { //★
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
//原索引+oldCap
else {
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
//原索引放到bucket里
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
//原索引+oldCap放到bucket里
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
相比于JDK1.7,1.8使用的是2次幂的扩展(指长度扩为原来2倍),所以,元素的位置要么是在原位置,要么是在原位置再移动2次幂的位置。我们在扩充HashMap的时候,不需要像JDK1.7的实现那样重新计算hash,只需要看看原来的hash值新增的那个bit是1还是0就好了,是0的话索引没变,是1的话索引变成“原索引+oldCap”
8 线程安全
|线程不安全_ab|
** HashMap线程安全吗?为什么不安全?不安全怎么办? |3**
-线程不安全的,其主要体现:
1)在jdk1.7中,在多线程环境下,扩容时会造成死循环(环形链)或数据丢失。-头插
2)在jdk1.8中,在多线程环境下,会发生数据覆盖的情况。-尾插
-过程分析:线程不安全
分析过程结合阅读
9 remove
-删除元素
首先,找到桶的位置,如果是链表,则进行链表遍历,找到需要删除的元素后,进行删除;如果是红黑树,也是进行树的遍历,找到元素删除后,进行平衡调节,
注意:红黑树的节点数小于 6 时,会转化成链表。
-遍历删除:
当遍历Map需要删除的时候,不可以for循环遍历,否则会产生并发修改异常CME,只能使用迭代器iterator.remove()来删除元素,或者使用线程安全的concurrentHashMap来删除Map中删除元素(concurrentHashMap和迭代器Iterator遍历删除)
10 get
public V get(Object key) {
Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
final Node<K,V> getNode(int hash, Object key) {
Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {
//根据key计算的索引检查第一个索引
if (first.hash == hash && // always check first node
((k = first.key) == key || (key != null && key.equals(k))))
return first;
//不是第一个节点
if ((e = first.next) != null) {
if (first instanceof TreeNode)//遍历树查找元素
return ((TreeNode<K,V>)first).getTreeNode(hash, key);
do {
//遍历链表查找元素
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
return null;
}
②、判断是否存在给定的 key 或者 value
public boolean containsKey(Object key) {
return getNode(hash(key), key) != null;
}
public boolean containsValue(Object value) {
Node<K,V>[] tab; V v;
if ((tab = table) != null && size > 0) {
//遍历桶
for (int i = 0; i < tab.length; ++i) {
//遍历桶中的每个节点元素
for (Node<K,V> e = tab[i]; e != null; e = e.next) {
if ((v = e.value) == value ||
(value != null && value.equals(v)))
return true;
}
}
}
return false;
}
-HashMap没有直接提供getNode接口给用户调用,而提供的get函数,而get函数就是通过getNode来取得元素的。
get(key)方法时获取key的hash值,计算hash&(n-1)得到在链表数组中的位置first=tab[hash&(n-1)],先判断first的key是否与参数key相等,不等就遍历后面的链表找到相同的key值返回对应的Value值即可
11 遍历
12 1.7 1.8区别
区别 :
(1)HashMap方法没有synchronized修饰,线程非安全,HashTable线程安全;
(2)HashMap允许key和value为null,而HashTable不允许
2.底层实现:数组+链表实现
jdk8开始链表高度到8、数组长度超过64,链表转变为红黑树,元素以内部类Node节点存在计算key的hash值,二次hash然后对数组长度取模,对应到数组下标,如果没有产生hash冲突(下标位置没有元素),则直接创建Node存入数组,如果产生hash冲突,先进行equal比较,相同则取代该元素,不同,则判断链表高度插入链表,链表高度达到8,并且数组长度到64则转变为红黑树,长度低于6则将红黑树转回链表key为null,存在下标0的位置
数组扩容
public class LinkedHashMap<K,V>
extends HashMap<K,V>
implements Map<K,V>
2 实现访问顺序排序
LinkedHashMap有5个构造方法,其中4个都是按插入顺序,只有一个是可以指定按访问顺序:
public LinkedHashMap(int initialCapacity, float loadFactor, boolean accessOrder)
其中参数accessOrder就是用来指定是否按访问顺序,如果为true,就是访问顺序。
3 使用按访问有序实现缓存
在LinkedHashMap添加元素后,会调用removeEldestEntry防范,传递的参数时最久没有被访问的键值对,如果方法返回true,这个最久的键值对就会被删除。LinkedHashMap中的实现总返回false,该子类重写后即可实现对容量的控制
public class LRUCache<K, V> extends LinkedHashMap<K, V> {
private int maxEntries;
public LRUCache(int maxEntries) {
super(16, 0.75f, true);
this.maxEntries = maxEntries;
}
@Override
protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
return size() > maxEntries;
}
}
使用该缓存:
LRUCache<String,Object> cache = new LRUCache<>(3);
cache.put("a","abstract");
cache.put("b","basic");
cache.put("c","call");
cache.get("a");
cache.put("d","滴滴滴");
System.out.println(cache); // 输出为:{c=call, a=abstract, d=滴滴滴}
public class LinkedHashSet<E>
extends HashSet<E>
implements Set<E>, Cloneable, java.io.Serializable {
类
public class TreeMap<K,V>
extends AbstractMap<K,V>
implements NavigableMap<K,V>, Cloneable, java.io.Serializable
4 进程间的通讯(IPC)
--共享内存是最快的IPC方式
--进程间通信(Java和os有区别):
① 消息队列(MessageQueue)
② 共享内存(SharedMemory) :实现
③ 信号量(Semphore)
④ 套接字(Socket)
⑤ 管道(PIPE)
⑥ 命名管道(FIFO)
线程进程(协程)区别? |3 结合具体的操作系统windows/mac/linux?
CPU内存模型
--CPU缓存可以分为一级缓存(L1)、二级缓存(L2)和三级缓存(L3),每一级缓存中所储存的全部数据都是下一级缓存的一部分。当CPU要读取一个缓存数据时,首先会从一级缓存中查找;如果没有找到,再从二级缓存中查找;如果还是没有找到,就从三级缓存或内存中查找。
--单核CPU:
如果是单核CPU运行多线程,多个线程同时访问进程中的共享数据,CPU 将共享变量加载到高速缓存后,不同线程在访问缓存数据的时候,都会映射到相同的缓存位置,这样即使发生线程的切换,缓存仍然不会失效。
--多核CPU:
如果是多核CPU运行多线程,每个核都有一个L1缓存,如果多个线程运行在不同的内核上访问共享变量时,每个内核的 L1 缓存将会缓存一份共享变量。
缓存一致性协议
MESI是四种缓存段状态的首字母缩写,任何多核系统中的缓存段都处于这四种状态之一。我将以相反的顺序逐个讲解,因为这个顺序更合理:
失效(Invalid)缓存段,要么已经不在缓存中,要么它的内容已经过时。为了达到缓存的目的,这种状态的段将会被忽略。一旦缓存段被标记为失效,那效果就等同于它从来没被加载到缓存中。
共享(Shared)缓存段,它是和主内存内容保持一致的一份拷贝,在这种状态下的缓存段只能被读取,不能被写入。多组缓存可以同时拥有针对同一内存地址的共享缓存段,这就是名称的由来。
独占(Exclusive)缓存段,和S状态一样,也是和主内存内容保持一致的一份拷贝。区别在于,如果一个处理器持有了某个E状态的缓存段,那其他处理器就不能同时持有它,所以叫“独占”。这意味着,如果其他处理器原本也持有同一缓存段,那么它会马上变成“失效”状态。
已修改(Modified)缓存段,属于脏段,它们已经被所属的处理器修改了。如果一个段处于已修改状态,那么它在其他处理器缓存中的拷贝马上会变成失效状态,这个规律和E状态一样。此外,已修改缓存段如果被丢弃或标记为失效,那么先要把它的内容回写到内存中——这和回写模式下常规的脏段处理方式一样。
一些问题:(多线程环境下尤其)
缓存一致性问题: 当多个处理器的运算任务都涉及同一块主内存区域时,将可能导致各自的缓存数据不一致的情况,如果真的发生这种情况,需要各个处理器访问缓存时都遵循一些协议,在读写时要根据协议来进行操作,这类协议有MSI、MESI等。
指令重排序问题: 为了使得处理器内部的运算单元能尽量被充分利用,处理器可能会对输入代码进行乱序执行(Out-Of-Order Execution)优化,处理器会在计算之后将乱序执行的结果重组,保证该结果与顺序执行的结果是一致的,但并不保证程序中各个语句计算的先后顺序与输入代码中的顺序一致。
问题:
假设有两个线程(线程1和线程2)分别执行下面的方法,x是共享变量:
public class Example {
int x = 0;
public void count() {
x++; //1
System.out.println(x)//2
}
}
多核CPU:如果是多核CPU运行多线程,每个核都有一个L1缓存,如果多个线程运行在不同的内核上访问共享变量时,每个内核的L1缓存将会缓存一份共享变量。
1,1 的运行结果:
谈谈JMM(sxt2) 参考:volitile部分
1 JMM基本概念 -P39
1) 概念
Java内存模型(Java Memory Model,JMM)本身是一种抽象的概念,并不真实存在,它描述的是一组规则或规范,通过这组规范定义了程序中各个变量(包括实例字段,静态字段和构成数组对象的元素)的访问方式。
-Java线程间的通信采用的是共享Java内存模型(简称JMM),JMM决定一个线程对共享变量的写入何时对另一个线程可见.
-JMM的必要性:线程安全
2)JMM三大特性:为线程安全得到保证
① 可见性:主内存有更改,工作内存第一时间被通知改变
② 原子性:
③ 有序性
3)JMM关于同步的规定
1、线程解锁前,必须把共享变量的值刷新回主内存;
2、线程加锁前,必须读取主内存的最新值到自己的工作内存;
3、加锁解锁是同一把锁;
4)Java内存模型把内存分成了两部分:线程栈区和堆区
5)由于JVM运行程序的实体是线程,而每个线程创建时JVM都会为其创建一个工作内存(有些地方称为栈空间),工作内存是每个线程的私有数据区域,而Java内存模型中规定所有的变量都存储在主内存,主内存是共享内存区域,所有的线程都可以访问,但线程对变量的操作(读取赋值等)必须在工作内存中进行,首先要将变量从主内存拷贝到自己的工作内存空间,然后对变量进行操作,操作完成后在将变量写回主内存,不能直接操作主内存中的变量,各个线程的工作内存中存储着主内存中的变量副本拷贝,因此不同的线程间无法访问对方的工作内存,线程间的通信必须通过主内存来完成。
-注:存储 硬盘<内存(<缓存Cache)
主内存、线程自己的工作内存
6)cpu缓存:
why:cpu在内部cpu寄存器中处理数据,cpu缓存在主内存、寄存器之间,空间小,访问速度比主内存快的多。解决:cpu操作主内存同一地址数据,内存处理数据慢,需要等待,可以在cpu缓存存储一份直接获取。
流程:cpu访问主存时,先读取一部分主存数据到CPU缓存(当然如果CPU缓存中存在需要的数据就会直接从缓存获取),进而在读取CPU缓存到寄存器,当CPU需要写数据到主存时,同样会先刷新寄存器中的数据到CPU缓存,然后再把数据刷新到主内存中。
7)重排序类型以及可能带来的问题
编译器优化重排:不改变单线程语义下,语句执行顺序
指令并行重排:不存在数据依赖,机器指令执行顺序
内存系统重排:三级缓存,内存与缓存的数据同步存在时间差,加载(load)和存储(store)执行顺序
8)JMM解决方案:
①原子性:
JVM自身提供的对基本数据类型读写操作;
方法级别或者代码块级别--synchronized或重入锁(ReentrantLock)
②可见性:
synchronized或volatile
③指令重排导致的可见性和有序性:
volatile解决,其另外一个作用就是禁止重排序优化
④ happens-before:
JMM内部定义的happens-before原则保证多线程环境下两个操作间的原子性、可见性以及有序性。
9)happens-before原则:
①程序顺序原则
②锁规则 解锁(unlock)操作必然发生在后续的同一个锁的加锁(lock)之前。
③volatile规则 volatile变量的写,先发生于读,这保证了volatile变量的可见性。
④线程启动规则 线程的start()方法先于它的每一个动作,即如果线程A在执行线程B的start方法之前修改了共享变量的值,那么当线程B执行start方法时,线程A对共享变量的修改对线程B可见
⑤传递性 A先于B ,B先于C 那么A必然先于C
⑥线程终止规则 线程的所有操作先于线程的终结,Thread.join()等待当前执行的线程终止。
⑦线程中断规则
interrupt()的调用先行发生于被中断线程的代码检测到中断事件的发生,Thread.interrupted()检测线程是否中断。
⑧对象终结规则 对象的构造函数执行,结束先于finalize()方法
2 说说volatile?(sxt2)
-volatile是Java提供的轻量级的同步机制(轻量级synchronized)
-三个特性:① 保证内存可见性 ② 不保证原子性 ③ 禁止指令重排序
(Volatile变量具有synchronized的可见性特性,但是不具备原子特性。防止指令重排。)
Ⅰ 可见性
1.① 保证内存可见性(sxt2)volatile有什么特点,怎么保证可见性的
volatile可以保证可见性,及时通知其他线程,主物理内存的值已被修改。
1)可见性的保证是基于CPU的内存屏障指令,抽象为happens-before原则,确保一个线程的修改能对其他线程是可见的。
2)volatile保证了修饰的共享变量在转换为汇编语言时,会加上一个以lock为前缀的指令,当CPU发现这个指令时,立即会做两件事情:
① 将当前内核中线程工作内存中该共享变量刷新到主存;
② 通知其他内核里缓存的该共享变量内存地址无效;
3)happens-before
① 作用:指定两个操作之间的执行顺序。即:如果A线程的写操作a与B线程的读操作b之间存在happens-before关系,尽管a操作和b操作在不同的线程中执行,但JMM向程序员保证a操作将对b操作可见。
②示例:A happens-before B: A操作的结果将对B可见,且A的执行顺序排在B之前。
-线程修改了volatile变量,先写到工作内存还是主内存
volatile修饰的变量在被修改后会处理器直接将结果stroe和write进主内存,同时使得其他线程的工作内存缓存失效,实现可见性
https://www.sohu.com/a/399318783_120591934
Ⅱ 非原子性
② 不保证原子性(sxt2)
1) 不保证原子性,会出现写丢失(写覆盖),线程太快
2)i++在多线程下是非线程安全的,如何不加synchronized解决?
volatile不保证原子性的原因?
例子:i++被拆分3个指令:(字节码)
Ⅰ 执行getfield拿到原始n;
Ⅱ 执行iadd进行加1;
Ⅲ 执行putfile写吧累加后的值写回
-写覆盖问题:拷贝回自己的内存空间,每个人都拿到0,写回到主内存时,线程1写回到的时候被挂起了,线程2歘的写回了。然后线程1恢复后又写回了一遍,把原来的1给覆盖了。
-解决:AtomicInteger 保证原子性
addAndget[++i]、getAndAdd[i++]
decrementAndGet、getAndDecrement【加1】
作用:
-volatile 的一个重要作用就是和CAS结合,保证了原子性,详细的可以参见 java.util.concurrent.atomic 包下的类,比如 AtomicInteger。
https://blog.csdn.net/weixin_40460171/article/details/106473323
多线程访问volatile long a变量?
--long存储的前32bit和后32bit可能不是同时更新
--volatile 除了保证可见性和有序性, 还解决了 long 类型和 double 类型数据的 8 字节赋值问题.
虚拟机规范中允许对 64 位数据类型, 分为 2 次 32 位的操作来处理, 当读取一个非 volatile 类型的 long 变量时,如果对该变量的读操作和写操作不在同一个线程中执行, 那么很有可能会读取到某个值得高 32 位和另一个值得低 32 位.
详细:
Ⅲ 禁止指令重排
③ 禁止指令重排序(sxt2)
为了提高性能,编译器和处理器常常会对指令进行重排序。一般分为如下3种:
处理器在进行指令重排时,必须考虑指令之间的数据依赖(数据依赖不可重排)
--重排示例2:
多线程环境中线程交替执行,由于编译器优化重排的存在,两个线程中使用的变量能否保证一致性是无法确定的,结果无法预测。
public class Test{
int a = 0;
boolean flag = flse;
public void method1(){
a = 1; // 这两个语句会发生编译器重排
flag = true;
}
public void method2(){
if(flag){
a = a + 5;
sout("retVale:" + a);
}
}
}
volatile如何禁止指令重排(sxt2)
内存屏障
--volatile实现进制指令重排优化,从而避免多线程环境下程序出现乱序执行的现象。
--首先了解一个概念,内存屏障(Memory Barrier)又称内存栅栏,是一个CPU指令,它的作用有两个:
① 保证特定操作的顺序
② 保证某些变量的内存可见性(利用该特性实现volatile的内存可见性)
由于编译器和处理器都能执行指令重排的优化,如果在指令间插入一条Memory Barrier则会告诉编译器和CPU,不管什么指令都不能和这条Memory Barrier指令重排序,也就是说 通过插入内存屏障禁止在内存屏障前后的指令执行重排序优化。 内存屏障另外一个作用是刷新出各种CPU的缓存数,因此任何CPU上的线程都能读取到这些数据的最新版本。(注,即可见性)
即就是过在Volatile的写和读的时候,加入屏障,防止出现指令重排的
(
在每个volatile写操作的前面插入一个StoreStore屏障;
在每个volatile写操作的后面插入一个StoreLoad屏障;
在每个volatile读操作的后面插入一个LoadLoad屏障;
在每个volatile读操作的后面插入一个LoadStore屏障。
注意:volatile写是在前面和后面分别插入内存屏障,而volatile读操作是在后面插入两个内存屏障
)
--线程安全获得保证
① 工作内存与主内存同步延迟现象导致的可见性问题
可通过synchronized或volatile关键字解决,他们都可以使一个线程修改后的变量立即对其它线程可见
② 对于指令重排导致的可见性问题和有序性问题
可以使用volatile关键字解决,因为volatile关键字的另一个作用就是禁止重排序优化
Ⅳ 怎么用
volatile在哪里使用(sxt2)
volatile 常用于多线程环境下的单次操作(单次读或者单次写)。
1) 双重检测(Double Check Lock,DCL):(https://blog.csdn.net/qq_38734403/article/details/106976266)
instance = new SingletonDemo();可以分为以下3步骤完成(伪代码)
memory = allocate(); //1.分配对象内存空间
instance(memory); //2.初始化对象
instance = memory; //3.设置instance指向刚分配的内存地址,此时instance != null
步骤2和步骤3 不存在数据依赖关系,而且无论重排前还是重排后程序的执行结果在单线程中没有改变,因此这种重排优化是允许的。
memory = allocate(); //1.分配对象内存空间
instance = memory; //3.设置instance指向刚分配的内存地址,此时instance != null,但是对象还没有初始化完成!
instance(memory); //2.初始化对象
但是指令重排只会保证穿行语义的执行的一致性(单线程),但并不会关心多线程间的语义一致性。所以当一条线程访问instance不为null时,由于instance实例未必已初始化完成,也就造成了线程安全问题。
1 synchronized在1.6之后的改动?
--JDK1.6 对锁的实现引入了大量的优化,如偏向锁、轻量级锁、自旋锁、适应性自旋锁、锁消除、锁粗化等技术来减少锁操作的开销。
--锁主要存在四种状态,依次是:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态,他们会随着竞争的激烈而逐渐升级。注意锁可以升级不可降级,这种策略是为了提高获得锁和释放锁的效率。
2 4种用法
1)修饰一个代码块:同步代码块
① synchronized(this){
② synchronized(obj){//指定要给某个对象加锁,// 关键字在代码块上,锁为括号里面的对象
作用范围:大括号{}括起来的代码
作用对象:调用这个代码块的对象;
2)修饰一个方法:同步方法
写法1:synchronized void method(){
写法2:public void method(){
synchronized(this) {
作用范围:整个方法
作用对象:调用这个方法的所有对象;
非static方法时,获取的是对象锁(即类的实例对象) ,类的实例的锁。
3)修饰一个静态的方法:
synchronized static void method() {
作用范围:整个静态方法
作用对象:这个类的所有对象;
获取的是类锁(即Class本身,注意:不是实例),类的Class对象的锁。
4)修饰一个类:
synchronized(ClassName.class) {
作用范围:synchronized后面括号括起来的部分
作用对象:这个类的所有对象。
4 synchornized的底层原理
--Synchronized实现同步锁的方式有两种,一种是修饰方法,一种是修饰方法块
--Java虚拟机中的同步(Synchronization)基于进入和退出管程(Monitor)对象实现,无论是显式同步还是隐式同步都是如此。
-- 显式同步-同步代码块:有明确的 monitorenter 和 monitorexit 指令,即同步代码块
-- 隐式同步-同步方法:synchronized修饰的同步方法是Java中同步用的最多;由方法调用指令读取运行时常量池中方法的ACC_SYNCHRONIZED标志来隐式实现的
显式同步-同步代码块
--由 monitorenter和 monitorexit 指令来实现同步的。
--进入 monitorenter 指令后,线程将持有 Monitor 对象,退出 monitorenter指令后,线程将释放该 Monitor 对象。
隐式同步-同步方法
--JVM使用了 ACC_SYNCHRONIZED访问标志来区分一个方法是否是同步方法。
--过程:当方法调用时,调用指令检查该方法是否被设置 ACC_SYNCHRONIZED 访问标志。
如果设置了该标志,执行线程将先持有 Monitor对象,然后再执行方法。在该方法运行期间,其它线程将无法获取到该Mointor 对象,当方法执行完成后,再释放该Monitor 对象。
Synchronized修饰方法是怎么实现锁原理
--JVM中的同步是基于进入和退出管程(Monitor)对象实现的。每个对象实例都会有一个Monitor,Monitor可以和对象一起创建、销毁。Monitor 是由 ObjectMonitor实现,而ObjectMonitor 是由 C++ 的 ObjectMonitor.hpp 文件实现。
--ObjectMonitor.hpp
ObjectMonitor() {
_header = NULL;
_count = 0; // 记录个数
_waiters = 0,
_recursions = 0;
_object = NULL;
_owner = NULL;
_WaitSet = NULL; // 处于 wait 状态的线程,会被加入到 _WaitSet
_WaitSetLock = 0 ;
_Responsible = NULL ;
_succ = NULL ;
_cxq = NULL ;
FreeNext = NULL ;
_EntryList = NULL ; // 处于等待锁 block 状态的线程,会被加入到该列表
_SpinFreq = 0 ;
_SpinClock = 0 ;
OwnerIsThread = 0 ;
}
1)变量
ObjectMonitor中有两个队列,_WaitSet 和 _EntryList,用来保存ObjectWaiter对象列表(每个等待锁的线程都会被封装成ObjectWaiter对象)
_owner指向持有ObjectMonitor对象的线程
2)流程
Monitor 是依靠底层操作系统的 Mutex Lock 来实现互斥的
--多个线程同时访问一段同步代码时,多个线程会先被存放在 EntryList集合中,处于block状态的线程,都会被加入到该列表。
--当线程获取到对象的Monitor后进入 _Owner区域并把monitor中的owner变量设置为当前线程同时monitor中的计数器count加1
①若线程申请 Mutex成功,则持有该Mutex,其它线程将无法获取到该Mutex。当前线程执行完毕也将释放monitor(锁)并复位变量的值,以便其他线程进入获取monitor(锁)
②若线程调用 wait()方法,将将释放当前持有的monitor,,释放当前持有的Mutex,owner变量恢复为null,count自减1,同时该线程会进入 WaitSet集合中,等待下一次被唤醒。如果当前线程顺利执行完方法,也将释放 Mutex。
3 synchronized可重入实现
--定义:当一个线程再次请求自己持有对象锁的临界资源时,这种情况属于重入锁。
--在一个线程调用synchronized方法的同时在其方法体内部调用该对象另一个synchronized方法,也就是说一个线程得到一个对象锁后再次请求该对象锁,是允许的,这就是synchronized的可重入性。
注意另外一种情况,当子类继承父类时,子类也是可以通过可重入锁调用父类的同步方法。
--实现:
synchronized是基于monitor实现的,因此每次重入,monitor中的计数器仍会加1。
每个锁关联一个线程持有者和一个计数器。当计数器为0时表示该锁没有被任何线程持有,那么任何线程都都可能获得该锁而调用相应方法。当一个线程请求成功后,JVM会记下持有锁的线程,并将计数器计为1。此时其他线程请求该锁,则必须等待。而该持有锁的线程如果再次请求这个锁,就可以再次拿到这个锁,同时计数器会递增。当线程退出一个synchronized方法/块时,计数器会递减,如果计数器为0则释放该锁。
其他锁优化
1)动态编译实现锁消除 / 锁粗化
除了锁升级优化,Java 还使用了编译器对锁进行优化。
--JIT 编译器在动态编译同步块的时候,借助了一种被称为逃逸分析的技术
--JIT 编译器在编译这个同步块的时候不会生成 synchronized 所表示的锁的申请与释放的机器码,即消除了锁的使用。
--JIT 编译器将会把这几个同步块合并为一个大的同步块,从而避免一个线程“反
复申请、释放同一个锁“所带来的性能开销。
2) 减小锁粒度
--将一个数组和队列对象拆成多个小对象,来降低锁竞争,提升并行
度。
--最经典的减小锁粒度的案例就是 JDK1.8 之前实现的 ConcurrentHashMap 版本。
--ConcurrentHashMap 就很很巧妙地使用了分段锁 Segment 来降低锁资源竞争。
final、finally、finalize区别?
final
final修饰类,表示该类不可以被继承
final修饰变量,表示该变量不可以被修改,只允许赋值一次
final修饰方法,表示该方法不可以被重写
finally
finally是java保证代码一定要被执行的一种机制。
比如try-finally或try-catch-finally,用来关闭JDBC连接资源,用来解锁等等
finalize
finalize是Object的一个方法,它的目的是保证对象在被垃圾收集前完成特定资源的回收。
分布式锁
分布式锁一般有三种实现方式:1.数据库锁;2.基于Redis的分布式锁;3. 基于ZooKeeper的分布式锁。
线程同步的方式?
① synchronized
② Condition
③ CountDownLatch、CyclicBarrier
java锁及实现
java锁机制
1) 所熟知的Java锁机制无非就是Sychornized 锁 和 Lock锁 (对象头知识,偏向锁,轻量级锁,重量级锁)
都有什么锁?说说乐观锁悲观锁是什么,怎么实现,volatile关键字,CAS,AQS原理及实现。
1)锁的分类:
1)死锁是什么(sxt2)
1)产生死锁主要的原因
① 系统资源不足、② 进程运行推进的顺序不合适、③ 资源分配不当
2) 代码、
class HoldLockThread implements Runnable{
private String lockA;
private String lockB;
public HoldLockThread(String lockA, String lockB) {
this.lockA = lockA;
this.lockB = lockB;
}
@Override
public void run() {
synchronized (lockA){
System.out.println(Thread.currentThread().getName() + "\t 自己持有" + lockA + "\t 尝试获得" + lockB);
try { TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { e.printStackTrace(); }
synchronized (lockB){
System.out.println(Thread.currentThread().getName() + "\t 自己持有" + lockA + "\t 尝试获得" + lockB);
}
}
}
}
public class DeadLockDemo {
public static void main(String[] args) {
String lockA = "lockA";
String lockB = "lockB";
new Thread(new HoldLockThread(lockA,lockB) ,"ThreadAAA").start();
new Thread(new HoldLockThread(lockB,lockA) ,"ThreadBBB").start();
}
}
打印:
打印
ThreadAAA 自己持有:lockA 尝试获得:lockB
ThreadBBB 自己持有:lockB 尝试获得:lockA
2)死锁怎么定位。
① jps 定位进程号
② jstack 找到死锁查看
linux ps -ef|grep XXxxxx ls -l
windows jps=java ps 只查看java jps -l
另:https://blog.csdn.net/zp357252539/article/details/104292521
如何解决死锁
解决死锁问题的方法是:一种是用synchronized,一种是用Lock显式锁实现。
线程排查
1. 排查CPU占满的Java线程
产生CPU100%的原因:某一程序一直占用CPU是导致CPU100%的原因,大概有以下几种情况:
1)Java 内存不够或溢出导致GC overhead问题, GC overhead 导致的CPU 100%问题;
2)死循环问题. 如常见的HashMap被多个线程并发使用导致的死循环, 或者死循环;
3)某些操作一直占用CPU
步骤:
1)jps 获取Java**进程的PID。
2)top -Hp PID 查看对应进程的哪个线程**占用CPU过高。该进程内最耗费CPU的线程
3)echo "obase=16;PID" | bc 将线程的PID转换为16进制,大写转换为小写。
4)jstack pid >> java.txt 导出CPU占用高进程的线程栈
jstack 2444 >stack.txt或者jstack 进程id | grep 16进制线程id
在Java.txt中查找转换成为16进制的线程PID。找到对应的线程栈。
辅助
命令参考
grep "99b" stack.txt -A 25
grep -C 5 foo file 显示file文件里匹配foo字串那行以及上下5行
grep -B 5 foo file 显示foo及前5行
grep -A 5 foo file 显示foo及后5行
对线程状态进行分析。
新建( new )、可运行( runnable )、运行( running )、阻塞( block )、死亡( dead )
如何预防死锁?
互斥、占有且等待、循环等待
不可抢占
为解决的问题:
死锁问题中,破坏不可抢占条件方案,但是这个方案synchronized没有办法解决。原因是synchronized申请资源的时候,如果申请不到,线程直接进入阻塞状态了,而线程进入阻塞状态,也释放不了线程已经占有的资源。但我们希望的是:
对于“不可抢占”这个条件,占用部分资源的线程进一步申请其他资源时,如果申请不到,可以主动释放它占有的资源,这样不可抢占这个条件就破坏掉了。
Java SDK 并发包里的Lock有别于synchronized隐式锁的三个特性:能够响应中断、支持超时和非阻塞地获取锁。
互斥锁lock三种方案:
1)能够响应中断。
synchronized 的问题是,持有锁 A 后,如果尝试获取锁B失败,那么线程就进入阻塞状态,一旦发生死锁,就没有任何机会来唤醒阻塞的线程。但如果阻塞状态的线程能够响应中断信号,也就是说当给阻塞的线程发送中断信号的时候,能够唤醒它,那它就有机会释放曾经持有的锁A。这样就破坏了不可抢占条件了。
2)支持超时。
如果线程在一段时间之内没有获取到锁,不是进入阻塞状态,而是返回一个错误,那这个线程也有机会释放曾经持有的锁。这样也能破坏不可抢占条件。
3)非阻塞地获取锁。如果尝试获取锁失败,并不进入阻塞状态,而是直接返回,那这个线程也有机会释放曾经持有的锁。这样也能破坏不可抢占条件。
--这三种方案可以全面弥补synchronized的问题。这三个方案体现在API上,就是 Lock 接口的三个方法。如下:
// 支持中断的 API
void lockInterruptibly()
throws InterruptedException;
// 支持超时的 API
boolean tryLock(long time, TimeUnit unit)
throws InterruptedException;
// 支持非阻塞获取锁的 API
boolean tryLock();
如何保证可见性?
--Java SDK 里面 Lock 的使用,有一个经典的范例,就是try{}finally{}
,需要重点关注的是在finally里面释放锁。
--可见性是怎么保证的?
--- Java 里多线程的可见性是通过 Happens-Before 规则保证的,
--- synchronized 之所以能够保证可见性,也是因为有一条 synchronized相关的规则:synchronized 的解锁Happens-Before于后续对这个锁的加锁。
--- Java SDK 里面 Lock 靠什么保证可见性呢?例如在下面的代码中,线程 T1 对 value 进行了 +=1 操作,那后续的线程 T2 能够看到 value的正确结果吗?
class X {
private final Lock rtl = new ReentrantLock();
int value;
public void addOne() {
// 获取锁
rtl.lock();
try {
value+=1;
} finally {
// 保证锁能释放
rtl.unlock();
}
}
}
答案必须是肯定的。Java SDK里面锁原理简述:利用了 volatile 相关的 Happens-Before 规则。Java SDK 里面的ReentrantLock,内部持有一个 volatile 的成员变量 state,获取锁的时候,会读写 state 的值;解锁的时候,也会读写 state的值(简化后的代码如下面所示)。也就是说,在执行 value+=1
之前,程序先读写了一次 volatile 变量 state,在执行 value+=1 之后,又读写了一次 volatile变量 state。根据相关的 Happens-Before 规则:
1)顺序性规则:对于线程 T1,value+=1 Happens-Before 释放锁的操作 unlock();
2)volatile 变量规则:由于 state = 1 会先读取 state,所以线程 T1 的 unlock() 操作Happens-Before 线程 T2 的 lock() 操作;
3)传递性规则:线程 T2 的 lock() 操作 Happens-Before 线程 T1 的 value+=1 。
class SampleLock {
volatile int state;
// 加锁
lock() {
// 省略代码无数
state = 1;
}
// 解锁
unlock() {
// 省略代码无数
state = 0;
}
}
所以说,后续线程 T2 能够看到 value 的正确结果
Condition
public class BlockedQueue<T>{
final Lock lock = new ReentrantLock();
// 条件变量:队列不满
final Condition notFull = lock.newCondition();
// 条件变量:队列不空
final Condition notEmpty = lock.newCondition();
// 入队
void enq(T x) {
lock.lock();
try {
while (队列已满){
// 等待队列不满
notFull.await();
}
// 省略入队操作...
// 入队后, 通知可出队
notEmpty.signal();
}finally {
lock.unlock();
}
}
// 出队
void deq(){
lock.lock();
try {
while (队列已空){
// 等待队列不空
notEmpty.await();
}
// 省略出队操作...
// 出队后,通知可入队
notFull.signal();
}finally {
lock.unlock();
}
}
}
注意:
-- Lock 和 Condition实现的管程,线程等待和通知需要调用await()、signal()、signalAll(),语义和wait()、notify()、notifyAll()是相同的。
-- 区别是,Lock&Condition实现的管程里只能使用前面的await()、signal()、signalAll(),而后面的wait()、notify()、notifyAll() 只有在 synchronized实现的管程里才能使用。
-- 如果一不小心在Lock&Condition实现的管程里调用了wait()、notify()、notifyAll(),那程序可就彻底玩儿完了。
1 操作系统线程状态
1) 死锁,Deadlock(重点关注)
2) 执行中,Runnable
3) 等待资源,Waiting on condition(重点关注,等待什么资源)
4) 等待获取监视器,Waiting on monitor entry(重点关注)
5) 暂停,Suspended
6) 对象等待中,Object.wait() 或 TIMED_WAITING
7) 阻塞,Blocked(重点关注)
8) 停止,Parked
2 Java线程的状态
在操作系统层面,Java线程中的BLOCKED、WAITING、TIMED_WAITING是一种状态,即休眠状态。Java线程处于这三种状态之一,那么这个线程就永远没有CPU的使用权。
3线程的状态及转换方式
RUNNABLE与BLOCKED的状态转换
-- RUNNABLE转BLOCKED:一种场景会触发,线程等待synchronized的隐式锁。
-- BLOCKED转RUNNABLE:当等待的线程获得synchronized隐式锁时,就又会从BLOCKED转换到RUNNABLE状态。
RUNNABLE与WAITING的状态转换:3种场景会触发
① 获得synchronized隐式锁的线程,调用无参数的 Object.wait() 方法。
② 调用无参数的Thread.join()方法。
join()是一种线程同步方法,例如有一个线程对象threadA,当调用A.join()的时候,执行这条语句的线程会等待threadA执行完,而等待中的这个线程,其状态会从 RUNNABLE 转换到 WAITING。当线程 thread A 执行完,原来等待它的线程又会从 WAITING 状态转换到 RUNNABLE。
③ 调用 LockSupport.park() 方法。
调用LockSupport.park()方法,当前线程会阻塞,线程的状态会从 RUNNABLE 转换到 WAITING。调用 LockSupport.unpark(Thread thread) 可唤醒目标线程,目标线程的状态又会从 WAITING 状态转换到 RUNNABLE。
RUNNABLE与TIMED_WAITING的状态转换:5种场景会触发
① 调用带超时参数的 Thread.sleep(long millis) 方法;
② 获得synchronized隐式锁的线程,调用带超时参数的Object.wait(long timeout) 方法;
③ 调用带超时参数的 Thread.join(long millis) 方法;
④ 调用带超时参数的 LockSupport.parkNanos(Object blocker, long deadline) 方法;
⑤ 调用带超时参数的 LockSupport.parkUntil(long deadline) 方法。
NEW到RUNNABLE状态
Java刚创建出来的Thread对象就是NEW状态。
从NEW状态转换到RUNNABLE状态,只要调用线程对象的start()方法就可以
从RUNNABLE到TERMINATED状态
-- 线程执行完run()方法后,会自动转换到TERMINATED状态,当然如果执行run()方法的时候异常抛出,也会导致线程终止。
-- 强制中断run()方法的执行,调用 interrupt()方法。
阻塞和等待的区别?
--定义+何时触发?
--BLOCKED:一个线程因为等待临界区的锁被阻塞产生的状态
--WAITING:一个线程进入了锁,但是需要等待其他线程执行某些操作。时间不确定
Object 的wait()/notify()/notifyAll() 的用法
Condition的 await()、signal()、signalAll()
1 CAS
1)CAS是什么
--概念:CAS 的全称 Compare-And-Swap即比较交换。它是一条 CPU 并发原语。
--功能:是判断内存某一个位置的值是否为预期,如果是则更改这个值,这个过程就是原子的。
--核心思想:执行函数:CAS(V,E,N)
3个参数:V表示要更新的变量,E表示预期值,N表示新值
如CAS(1,1,3)=> 1=1,则将1置为3
--CAS 并发原语现在 JAVA 语言中就是 sun.misc.Unsafe 类中的各个方法。调用 UnSafe 类中的 CAS 方法,JVM 会帮我们实现出 CAS 汇编指令。这是一种完全依赖硬件的功能,通过它实现了原子操作。由于 CAS 是一种系统原语,原语属于操作系统用语范畴,是由若干条指令组成,用于完成某一个功能的过程,并且原语的执行必须是连续的,在执行的过程中不允许被中断,也就是说 CAS 是一条原子指令,不会造成所谓的数据不一致的问题。(即线程安全)
2)UnSafe:JVM的原始类,部分属性方法native修饰
--Unsafe 是 CAS 的核心类,由于 Java 方法无法直接访问底层系统,而需要通过本地(native)方法来访问, Unsafe 类相当一个后门,基于该类可以直接操作特定内存的数据。Unsafe 类存在于 sun.misc 包中,其内部方法操作可以像 C 指针一样直接操作内存,因为 Java 中 CAS 操作执行依赖于 Unsafe 类。
--变量 vauleOffset,表示该变量值在内存中的偏移量,因为 Unsafe 就是根据内存偏移量来获取数据原值的。
--变量 value 用 volatile 修饰,保证了多线程之间的内存可见性,value是同一份。
--Unsafe类中的compareAndSwapInt,是一个本地方法,实现位于unsafe.cpp中。
2.1)Unsafe类一些属性、方法:
--类和实例对象以及变量的操作:
//获得给定对象的指定偏移量offset的int值,使用volatile语义,总能获取到最新的int值。
public native int getIntVolatile(Object o, long offset);
--Unsafe类中CAS 操作相关:
Java中无锁操作CAS基于以下3个方法实现,在Atomic系列内部方法是基于下述方法的实现的。
//第一个参数o为给定对象,offset为对象内存的偏移量,通过这个偏移量迅速定位字段并设置或获取该字段的值,
//expected表示期望值,x表示要设置的值,下面3个方法都通过CAS原子指令执行操作。
public final native boolean compareAndSwapObject(Object o, long offset,Object expected, Object x);
public final native boolean compareAndSwapInt(Object o, long offset,int expected,int x);
public final native boolean compareAndSwapLong(Object o, long offset,long expected,long x);
--指针类Unsafe类中JDK 1.8新增的几个方法,它们的实现是基于上述的CAS方法 int型为例 非native
//1.8新增,给定对象o,根据获取内存偏移量指向的字段,将其增加delta,
//这是一个CAS操作过程,直到设置成功方能退出循环,返回旧值
public final int getAndAddInt(Object o, long offset, int delta) {
int v;
do {
//获取内存中最新值
v = getIntVolatile(o, offset);
//通过CAS操作
} while (!compareAndSwapInt(o, offset, v, v + delta));
return v;
}
//1.8新增,方法作用同上,只不过这里操作的long类型数据
public final long getAndAddLong(Object o, long offset, long delta) {...}
//1.8新增,给定对象o,根据获取内存偏移量对于字段,将其 设置为新值newValue,
//这是一个CAS操作过程,直到设置成功方能退出循环,返回旧值
public final int getAndSetInt(Object o, long offset, int newValue) {
int v;
do {
v = getIntVolatile(o, offset);
} while (!compareAndSwapInt(o, offset, v, newValue));
return v;
}
// 1.8新增,同上,操作的是long类型
public final long getAndSetLong(Object o, long offset, long newValue) {...}
//1.8新增,同上,操作的是引用类型数据
public final Object getAndSetObject(Object o, long offset, Object newValue) {...}
--挂起与恢复(park unpark os 线程挂起)
--内存屏障(这个volitile有用到)
2 Atomic
并发包中的原子操作类(Atomic系列),从JDK 1.5开始提供了java.util.concurrent.atomic包,在该包中提供了许多基于CAS实现的原子操作类
原子更新基本类型
1)3个基本类型
AtomicBoolean:原子更新布尔类型
AtomicInteger:原子更新整型
AtomicLong:原子更新长整型
这3个类的实现原理和使用方式几乎是一样的,这里我们以AtomicInteger为例进行分析
2)AtomicInteger
public class AtomicInteger extends Number implements java.io.Serializable {
...
// 获取指针类Unsafe
private static final Unsafe unsafe = Unsafe.getUnsafe();
//下述变量value在AtomicInteger实例对象内的内存偏移量
private static final long valueOffset;
static {
try {
//通过unsafe类的objectFieldOffset()方法,获取value变量在对象内存中的偏移
//通过该偏移量valueOffset,unsafe类的内部方法可以获取到变量value对其进行取值或赋值操作
valueOffset = unsafe.objectFieldOffset
(AtomicInteger.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }
}
//当前AtomicInteger封装的int变量value
private volatile int value;
...
//当前值加1,返回新值,底层CAS操作
public final int incrementAndGet() {
return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}
...
}
--重点分析自增操作方法实现过程,其他方法自增实现原理一样。
--发现AtomicInteger类中所有自增或自减的方法都间接调用Unsafe类中的getAndAddInt()方法实现了CAS操作,从而保证了线程安全,关于getAndAddInt,是Unsafe类中1.8新增的方法,源码如下
//Unsafe类中的getAndAddInt方法
public final int getAndAddInt(Object o, long offset, int delta) {
int v;
do {
v = getIntVolatile(o, offset);
} while (!compareAndSwapInt(o, offset, v, v + delta));
return v;
}
--getAndAddInt通过一个while循环不断的重试更新要设置的值,直到成功为止,调用的是Unsafe类中的compareAndSwapInt方法,是一个CAS操作方法。注意的是,上述源码分析是基于JDK1.8的,如果是1.8之前的方法,AtomicInteger源码实现有所不同,是基于for死循环的,如下
//JDK 1.7的源码,由for的死循环实现,并且直接在AtomicInteger实现该方法,
//JDK1.8后,该方法实现已移动到Unsafe类中,直接调用getAndAddInt方法即可
public final int incrementAndGet() {
for (;;) {
int current = get();
int next = current + 1;
if (compareAndSet(current, next))
return next;
}
}
① atomicInteger.getAndIncreament:解决i++线程安全问题
--方法调用的 unsafe.getAndAddInt(this,valueoffset,1)
this:当前对象
valueoffset:内存偏移量,即内存地址
getAndAddInt:在unsafe类,实现使用了先获取当前地址值getIntVolatile,再比较交换compareAndSwapInt,没得到正确值会一直CAS
3 原子更新引用-AtomicReference
AtomicReference原子类,即原子更新引用类型。
AtomicReference原子类内部是如何实现CAS操作的呢?
-- AtomicReference与AtomicInteger的实现原理基本是一样的,最终执行的还是Unsafe类,关于AtomicReference的其他方法也是一样的
public class AtomicReference<V> implements java.io.Serializable {
private static final Unsafe unsafe = Unsafe.getUnsafe();
private static final long valueOffset;
static {
try {
valueOffset = unsafe.objectFieldOffset
(AtomicReference.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }
}
//内部变量value,Unsafe类通过valueOffset内存偏移量即可获取该变量
private volatile V value;
//CAS方法,间接调用unsafe.compareAndSwapObject(),它是一个
//实现了CAS操作的native方法
public final boolean compareAndSet(V expect, V update) {
return unsafe.compareAndSwapObject(this, valueOffset, expect, update);
}
//设置并获取旧值
public final V getAndSet(V newValue) {
return (V)unsafe.getAndSetObject(this, valueOffset, newValue);
}
//省略其他代码......
}
//Unsafe类中的getAndSetObject方法,实际调用还是CAS操作
public final Object getAndSetObject(Object o, long offset, Object newValue) {
Object v;
do {
v = getObjectVolatile(o, offset);
} while (!compareAndSwapObject(o, offset, v, newValue));
return v;
}
4 CAS缺点
代码如下:
public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
do {
var5 = this.getIntVolatile(var1, var2);
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
return var5;
}
1)循环时间长开销很大:
有个do-while,CAS失败会一直尝试,会给CPU带来很大开销,效率低于 synchronized
2)只能保证一个共享变量的原子操作
多共享变量,循环CAS会破坏原子性,只能加锁
3)ABA问题
5 ABA问题解决
原子类AtomicInterger的ABA问题?原子更新引用知道吗?
1)CAS会导致ABA问题
--CAS算法实现一个重要前提需要取出内存中某时刻的数据并在当下时刻比较并替换,那么在这个时间差类会导致数据的变化。
--示例:(一个慢取出后挂起,快的已经更改了值,慢的再用原来的值更改)
有线程one,two两个线程,one线程较慢需要十秒钟,two线程较快尽需两秒,
一个线程one从内存位置中取出A,这时候另一个线程two也从内存中取出A,并且线程two进行了一些操作将值变成了B,然后线程内有一些其他操作two又将V位置的数据变成A,这时候线程one进行CAS操作发现内存中仍然是A,然后线程one操作成功。
--尽管线程one的CAS操作成功,但是不代表这个过程就是没有问题的。因为one得到的这个内存中的值已经发生了许多问题.
2)原子引用:AotmicReference类
AotmicReference<User> aotmicReference = new AotmicReference<>();
aotmicReference.set();
aotmicReference.compareAndSet();
3)时间戳原子引用
AtomicStampedReference类,boolean
atomicStampedReference=new AtomicStampedReference<>(值,时间戳-版本号)
atomicStampedReference.compareAndSet(现值,期望值,期望版本号,新版本号)
AtomicStampedReference
--概念:
一个带有时间戳的对象引用,在每次修改后,AtomicStampedReference不仅会设置新值而且还会记录更改的时间。当AtomicStampedReference设置对象值时,对象值以及时间戳都必须满足期望值才能写入成功。
--内部实现思想:
通过一个键值对Pair存储数据和时间戳,在更新时对数据和时间戳进行比较,只有两者都符合预期才会调用Unsafe的compareAndSwapObject方法执行数值和时间戳替换,也就避免了ABA的问题。
--内部实现原理:
public class AtomicStampedReference<V> {
//通过Pair内部类存储数据和时间戳
private static class Pair<T> {
final T reference;
final int stamp;
private Pair(T reference, int stamp) {
this.reference = reference;
this.stamp = stamp;
}
static <T> Pair<T> of(T reference, int stamp) {
return new Pair<T>(reference, stamp);
}
}
//存储数值和时间的内部类
private volatile Pair<V> pair;
//构造器,创建时需传入初始值和时间初始值
public AtomicStampedReference(V initialRef, int initialStamp) {
pair = Pair.of(initialRef, initialStamp);
}
}
...
// 接着看看其compareAndSet方法的实现
public boolean compareAndSet(V expectedReference,
V newReference,
int expectedStamp,
int newStamp) {
Pair<V> current = pair;
return
expectedReference == current.reference &&
expectedStamp == current.stamp &&
((newReference == current.reference &&
newStamp == current.stamp) ||
casPair(current, Pair.of(newReference, newStamp)));
}
看下casPair():
同时对当前数据和当前时间进行比较,只有两者都相等是才会执行casPair()方法,单从该方法的名称就可知是一个CAS方法,最终调用的还是Unsafe类中的compareAndSwapObject方法:
private boolean casPair(Pair<V> cmp, Pair<V> val) {
return UNSAFE.compareAndSwapObject(this, pairOffset, cmp, val);
}
从 Java1.5 开始 JDK 的 atomic包里提供了一个类 AtomicStampedReference 来解决 ABA 问题。
手写一个自旋锁(sxt2) AtomicReference实现
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
/**
* 题目:实现一个自旋锁
* 自旋锁好处:循环比较获取直至成功为止,没有类似wait的阻塞。
*
* 通过CAS操作完成自旋锁,A线程先进来调用myLock方法,自己持有5秒钟,
* B随后进来后发现,当前线程持有锁,不是null,
* 所以只能通过自旋等待,直到A释放锁后B随后抢到。
*/
public class SpinLockDemo {
// 原子引用线程
AtomicReference<Thread> atomicReference = new AtomicReference<>();
public void myLock(){
Thread thread = Thread.currentThread(); // 当前进来的线程
System.out.println(thread.getName() + "\t come in!");
while(!atomicReference.compareAndSet(null,thread)){//期望值,现值为null,当前线程进去
}
}
// 解锁
public void myUnlock(){
Thread thread = Thread.currentThread();
atomicReference.compareAndSet(thread,null); // 用完,设置为null
System.out.println(thread.getName() + "\t invoked myUnlock()");
}
public static void main(String[] args) {
SpinLockDemo spinLockDemo = new SpinLockDemo();
new Thread(()->{
spinLockDemo.myLock();
// 暂停一会线程
try {TimeUnit.SECONDS.sleep(5);} catch (InterruptedException e) {e.printStackTrace();}
spinLockDemo.myUnlock();
},"AA").start();
// 保证A先启动
try {TimeUnit.SECONDS.sleep(1);} catch (InterruptedException e) {e.printStackTrace();}
new Thread(()->{
spinLockDemo.myLock();
spinLockDemo.myUnlock();
},"BB").start();
}
}
ublic class SpinLock {
private AtomicReference cas = new AtomicReference();
public void lock() {
Thread current = Thread.currentThread();
// 利用CAS
while (!cas.compareAndSet(null, current)) {
// DO nothing
}
}
public void unlock() {
Thread current = Thread.currentThread();
cas.compareAndSet(current, null);
}
}
lock()方法利用的CAS,当第一个线程A获取锁的时候,能够成功获取到,不会进入while循环,如果此时线程A没有释放锁,另一个线程B又来获取锁,此时由于不满足CAS,所以就会进入while循环,不断判断是否满足CAS,直到A线程调用unlock方法释放了该锁。
https://www.jianshu.com/p/9d3660ad4358
可重入锁概念:
-- ReentrantLock翻译叫可重入锁。所谓可重入锁,顾名思义,指的是线程可以重复获取同一把锁。
-- 同一个线程外层函数获得锁之后,内层递归函数仍然能够获取该锁的代码,在同一个线程在外层方法获取锁的时候,在进入内层方法会自动获取锁
-- 如下代码,当线程 T1 执行到 ①处时,已经获取到了锁 rtl ,当在 ① 处调用get() 方法时,会在 ② 再次对锁 rtl执行加锁操作。
此时,如果锁 rtl 是可重入的,那么线程T1可以再次加锁成功;如果锁 rtl 是不可重入的,那么线程 T1 此时会被阻塞。
class X {
private final Lock rtl = new ReentrantLock();
int value;
public int get() {
// 获取锁
rtl.lock(); ②
try {
return value;
} finally {
// 保证锁能释放
rtl.unlock();
}
}
public void addOne() {
// 获取锁
rtl.lock();
try {
value = 1 + get(); ①
} finally {
// 保证锁能释放
rtl.unlock();
}
}
}
AQS底层使用了模板方法模式
同步器的设计是基于模板方法模式的,如果需要自定义同步器一般的方式是这样(模板方法模式很经典的一个应用):
使用者继承AbstractQueuedSynchronizer并重写指定的方法。(这些重写方法很简单,无非是对于共享资源state的获取和释放)
将AQS组合在自定义同步组件的实现中,并调用其模板方法,而这些模板方法会调用使用者重写的方法。
这和我们以往通过实现接口的方式有很大区别,这是模板方法模式很经典的一个运用。
AQS使用了模板方法模式,自定义同步器时需要重写下面几个AQS提供的模板方法:
isHeldExclusively()//该线程是否正在独占资源。只有用到condition才需要去实现它。
tryAcquire(int)//独占方式。尝试获取资源,成功则返回true,失败则返回false。
tryRelease(int)//独占方式。尝试释放资源,成功则返回true,失败则返回false。
tryAcquireShared(int)//共享方式。尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。
tryReleaseShared(int)//共享方式。尝试释放资源,成功则返回true,失败则返回false。
默认情况下,每个方法都抛出 UnsupportedOperationException。 这些方法的实现必须是内部线程安全的,并且通常应该简短而不是阻塞。AQS类中的其他方法都是final ,所以无法被其他类使用,只有这几个方法可以被其他类使用。
*以ReentrantLock为例,state初始化为0,表示未锁定状态。A线程lock()时,会调用tryAcquire()独占该锁并将state+1。此后,其他线程再tryAcquire()时就会失败,直到A线程unlock()到state=0(即释放锁)为止,其它线程才有机会获取该锁。当然,释放锁之前,A线程自己是可以重复获取此锁的(state会累加),这就是可重入的概念。但要注意,获取多少次就要释放多么次,这样才能保证state是能回到零态的。
*再以CountDownLatch以例,任务分为N个子线程去执行,state也初始化为N(注意N要与线程个数一致)。这N个子线程是并行执行的,每个子线程执行完后countDown()一次,state会CAS(Compare and Swap)减1。等到所有子线程都执行完后(即state=0),会unpark()主调用线程,然后主调用线程就会从await()函数返回,继续后余动作。
*一般来说,自定义同步器要么是独占方法,要么是共享方式,他们也只需实现tryAcquire-tryRelease、tryAcquireShared-tryReleaseShared中的一种即可。但AQS也支持自定义同步器同时实现独占和共享两种方式,如ReentrantReadWriteLock。
-----------------------------具体-------------------
AQS的原理概要,如下源码
2 AQS中的同步队列模型
1)AQS
--head和tail:分别是AQS中的变量。
head:指向同步队列的头部,注意head为空结点,不存储信息。
tail:指向同步队列的队尾,同步队列采用的是双向链表的结构这样可方便队列进行结点增删操作。
--state: state变量则是代表同步状态。
state=0:执行当线程调用lock方法进行加锁后,如果此时state的值为0,则说明当前线程可以获取到锁(在本篇文章中,锁和同步状态代表同一个意思),同时将state设置为1,表示获取成功。
state=1:如果state已为1,也就是当前锁已被其他线程持有,那么当前执行线程将被封装为Node结点加入同步队列等待。
--Node结点:是对每一个访问同步代码的线程的封装。
/** AQS抽象类*/
public abstract class AbstractQueuedSynchronizer
extends AbstractOwnableSynchronizer{
//指向同步队列队头
private transient volatile Node head;
//指向同步的队尾
private transient volatile Node tail;
//同步状态,0代表锁未被占用,1代表锁已被占用
private volatile int state;
//省略其他代码......
}
2)Node节点
从图中的Node的数据结构也可看出,其包含了需要同步的线程本身以及线程的状态,如是否被阻塞,是否等待唤醒,是否已经被取消等。每个Node结点内部关联其前继结点prev和后继结点next,这样可以方便线程释放锁后快速唤醒下一个在等待的线程,Node是AQS的内部类,其数据结构如下:
-- SHARED(shared)和EXCLUSIVE(exclusive)常量:分别代表共享模式和独占模式。
① 共享模式:是一个锁允许多条线程同时操作;
如信号量Semaphore采用的就是基于AQS的共享模式实现的。
② 独占模式:是同一个时间段只能有一个线程对共享资源进行操作,多余的请求线程需要排队等待;
如ReentranLock。
--waitStatus变量:表示当前被封装成Node结点的等待状态。
共4种:
① CANCELLED:值为1,在同步队列中等待的线程等待超时或被中断,需要从同步队列中取消该Node的结点,其结点的waitStatus为CANCELLED,即结束状态,进入该状态后的结点将不会再变化。
② SIGNAL:值为-1,被标识为该等待唤醒状态的后继结点,当其前继结点的线程释放了同步锁或被取消,将会通知该后继结点的线程执行。说白了,就是处于唤醒状态,只要前继结点释放锁,就会通知标识为SIGNAL状态的后继结点的线程执行。
③ CONDITION:值为-2,与Condition相关,该标识的结点处于等待队列中,结点的线程等待在Condition上,当其他线程调用了Condition的signal()方法后,CONDITION状态的结点将从等待队列转移到同步队列中,等待获取同步锁。
④ PROPAGATE:值为-3,与共享模式相关,在共享模式中,该状态标识结点的线程处于可运行状态。
⑤ 0状态:值为0,代表初始化状态。
--pre和next:分别指向当前Node结点的前驱结点和后继结点;
--thread变量:存储的请求锁的线程。
--nextWaiter:与Condition相关,代表等待队列中的后继结点,后续会有更详细的分析。
static final class Node {
static final Node SHARED = new Node(); //共享模式
static final Node EXCLUSIVE = null; //独占模式
static final int CANCELLED = 1; //标识线程已处于结束状态
static final int SIGNAL = -1; //等待被唤醒状态
static final int CONDITION = -2; //条件状态,
static final int PROPAGATE = -3; //在共享模式中使用表示获得的同步状态会被传播
volatile int waitStatus; //等待状态,存在CANCELLED、SIGNAL、
//CONDITION、PROPAGATE 4种
volatile Node prev; //同步队列中前驱结点
volatile Node next; //同步队列中后继结点
volatile Thread thread; //请求锁的线程
Node nextWaiter; //等待队列中的后继结点,这个与Condition有关
final boolean isShared() { //判断是否为共享模式
return nextWaiter == SHARED;
}
final Node predecessor() throws NullPointerException { //获取前驱结点
Node p = prev;
if (p == null)
throw new NullPointerException();
else
return p;
}
//.....
}
3)总结
总之呢,AQS作为基础组件,对于锁的实现存在两种不同的模式,即共享模式(如Semaphore)和独占模式(如ReetrantLock),无论是共享模式还是独占模式的实现类,其内部都是基于AQS实现的,也都维持着一个虚拟的同步队列,当请求锁的线程超过现有模式的限制时,会将线程包装成Node结点并将线程当前必要的信息存储到node结点中,然后加入同步队列等会获取锁,而这系列操作都有AQS协助我们完成,这也是作为基础组件的原因,无论是Semaphore还是ReetrantLock,其内部绝大多数方法都是间接调用AQS完成的。
下面是AQS整体类图结构:
4)ReentrantLock与AQS的关系
1> ReentrantLock类和继承:
--AbstractOwnableSynchronizer:抽象类,定义了存储独占当前锁的线程和获取的方法
--AbstractQueuedSynchronizer:抽象类,AQS框架核心类,其内部以虚拟队列的方式管理线程的锁获取与锁释放,其中获取锁(tryAcquire方法)和释放锁(tryRelease方法)并没有提供默认实现,需要子类重写这两个方法实现具体逻辑,目的是使开发人员可以自由定义获取锁以及释放锁的方式。
--Node:AbstractQueuedSynchronizer 的内部类,用于构建虚拟队列(链表双向链表),管理需要获取锁的线程。
--Sync:抽象类,是ReentrantLock的内部类,继承自AbstractQueuedSynchronizer,实现了释放锁的操作(tryRelease()方法),并提供了lock抽象方法,由其子类实现。
--NonfairSync:是ReentrantLock的内部类,继承自Sync,非公平锁的实现类。
--FairSync:是ReentrantLock的内部类,继承自Sync,公平锁的实现类。
--ReentrantLock:实现了Lock接口的,其内部类有Sync、NonfairSync、FairSync,在创建时可以根据fair参数决定创建NonfairSync(默认非公平锁)还是FairSync。
2> ReentrantLock内部类:
--ReentrantLock内部存在3个实现类,分别是Sync、NonfairSync、FairSync。
--ReentrantLock的所有方法调用都通过间接调用AQS和Sync类及其子类来完成的。
--Sync类:继承自AQS实现了解锁tryRelease()方法;
--NonfairSync(非公平锁)、 FairSync(公平锁)则继承自Sync,实现了获取锁的tryAcquire()方法;
3> AQS
--AQS提供功能:
AQS是一个抽象类,但其源码中并没一个抽象的方法,这是因为AQS只是作为一个基础组件,并不希望直接作为直接操作类对外输出,而更倾向于作为基础组件,为真正的实现类提供基础设施,如构建同步队列,控制同步状态等,事实上,从设计模式角度来看,AQS采用的模板模式的方式构建的,其内部除了提供并发操作核心方法以及同步队列操作外,还提供了一些模板方法让子类自己实现,如加锁操作以及解锁操作,为什么这么做?
--为什么?设计理念:
这是因为AQS作为基础组件,封装的是核心并发操作,但是实现上分为两种模式,即共享模式与独占模式,而这两种模式的加锁与解锁实现方式是不一样的,但AQS只关注内部公共方法实现并不关心外部不同模式的实现,所以提供了模板方法给子类使用,也就是说实现独占锁,
如ReentrantLock需要自己实现tryAcquire()方法和tryRelease()方法,而实现共享模式的Semaphore,则需要实现tryAcquireShared()方法和tryReleaseShared()方法,
--好处:无论是共享模式还是独占模式,其基础的实现都是同一套组件(AQS),只不过是加锁解锁的逻辑不同罢了,更重要的是如果我们需要自定义锁的话,也变得非常简单,只需要选择不同的模式实现不同的加锁和解锁的模板方法即可,AQS提供给独占模式和共享模式的模板方法如下
//AQS中提供的主要模板方法,由子类实现。
public abstract class AbstractQueuedSynchronizer
extends AbstractOwnableSynchronizer{
protected boolean tryAcquire(int arg) { //独占模式下获取锁的方法
throw new UnsupportedOperationException();
}
protected boolean tryRelease(int arg) { //独占模式下解锁的方法
throw new UnsupportedOperationException();
}
protected int tryAcquireShared(int arg) { //共享模式下获取锁的方法
throw new UnsupportedOperationException();
}
protected boolean tryReleaseShared(int arg) { //共享模式下解锁的方法
throw new UnsupportedOperationException();
}
protected boolean isHeldExclusively() { //判断是否为持有独占锁
throw new UnsupportedOperationException();
}
}
ReetrantLock是基于AQS并发框架实现
ReentrantLock实现公平和非公平锁
//方法1:无参构造函数:默认非公平锁
public ReentrantLock() {
sync = new NonfairSync(); // 非公平锁
}
// 方法2:true时为公平锁,false时为非公平锁
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
在入口等待队列,锁都对应着一个等待队列,如果一个线程没有获得锁,就会进入等待队列,当有线程释放锁的时候,就需要从等待队列中唤醒一个等待的线程。
-- 如果是公平锁,唤醒的策略就是谁等待的时间长,就唤醒谁,很公平;
-- 如果是非公平锁,则不提供这个公平保证,有可能等待时间短的线程反而先被唤醒。
----------------------------具体-------------------------------
1 ReetrantLock中非公平锁-lock
--AQS实现:
AQS同步器的实现依赖于内部的同步队列(FIFO的双向链表对列)完成对同步状态(state)的管理,当前线程获取锁(同步状态)失败时,AQS会将该线程以及相关等待信息包装成一个节点(Node)并将其加入同步队列,同时会阻塞当前线程,当同步状态释放时,会将头结点head中的线程唤醒,让其尝试获取同步状态。
--这里重点分析一下获取同步状态和释放同步状态以及如何加入队列的具体操作,这里从ReetrantLock入手分析AQS的具体实现,先以非公平锁为例进行分析。
--非公平锁
public ReentrantLock() { //默认构造,创建非公平锁NonfairSync
sync = new NonfairSync();
}
public ReentrantLock(boolean fair) { //根据传入参数创建锁类型
sync = fair ? new FairSync() : new NonfairSync();
}
public void lock() { //加锁操作 √
sync.lock();
}
--sync是个抽象类:
存在两个不同的实现子类,从非公平锁NonfairSync子类入手:流程:
1)lock加锁
获取锁时,首先对同步状态执行CAS操作,尝试把state的状态从0设置为1 ->
① 返回true:则代表获取同步状态成功,也就是当前线程获取锁成,可操作临界资源;
② 返回false: 则表示已有线程持有该同步状态(其值为1),获取锁失败,注意这里存在并发的情景,也就是可能同时存在多个线程设置state变量,因此是CAS操作保证了state变量操作的原子性。
/**非公平锁实现*/
static final class NonfairSync extends Sync {
final void lock() { //加锁
if (compareAndSetState(0, 1)) //执行CAS操作,获取同步状态
//成功则将独占锁线程设置为当前线程
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1); //否则再次请求同步状态
}
}
2)lock->acquire(1)
返回false后,执行 acquire(1)-AQS方法,该方法是AQS中的方法,它对中断不敏感,即使线程获取同步状态失败,进入同步队列,后续对该线程执行中断操作也不会从同步队列中移出,方法如下
---传入参数arg:表示要获取同步状态后设置的值(即要设置state的值);
因为要获取锁,而status为0时是释放锁,1则是获取锁,所以一般传递参数为1,进入方法后首先会执行tryAcquire(arg)-ReetrantLock方法;
在前面分析过该方法在AQS中并没有具体实现,而是交由子类实现,因此该方法是由ReetrantLock类内部实现的
public final void acquire(int arg) { //再次尝试获取同步状态
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
3)tryAcquire(arg)
--tryAcquire(arg)在ReetrantLock的实现
--做了两件事:
① 尝试再次获取同步状态,如果获取成功则将当前线程设置为OwnerThread,否则失败;
② 判断当前线程current是否为OwnerThread,如果是则属于重入锁,state自增1,并获取锁成功,返回true,反之失败,返回false,也就是tryAcquire(arg)执行失败,返回false。
--注意:与公平锁不同的点:
nonfairTryAcquire(int acquires)内部使用的是CAS原子性操作设置state值,可以保证state的更改是线程安全的,因此只要任意一个线程调用nonfairTryAcquire(int acquires)方法并设置成功即可获取锁,不管该线程是新到来的还是已在同步队列的线程;
非公平锁特性,并不保证同步队列中的线程一定比新到来线程请求(可能是head结点刚释放同步状态然后新到来的线程恰好获取到同步状态)先获取到锁。
//1 NonfairSync类
static final class NonfairSync extends Sync {
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires); //由nonfairTryAcquire实现
}
}
//2 Sync类
abstract static class Sync extends AbstractQueuedSynchronizer {
final boolean nonfairTryAcquire(int acquires) { //nonfairTryAcquire方法
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) { //判断同步状态是否为0,并尝试再次获取同步状态
if (compareAndSetState(0, acquires)) { //执行CAS操作
setExclusiveOwnerThread(current);
return true;
}
}
//如果当前线程已获取锁,属于重入锁,再次获取锁后将status值加1
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
//设置当前同步状态,当前只有一个线程持有锁,因为不会发生线程安全问题,可以直接执行 setState(nextc);
setState(nextc);
return true;
}
return false;
}
//省略其他代码
}
4)再看acquire(int arg)
--理想情况:tryAcquire(arg)返回true,acquireQueued不执行,因为毕竟当前线程已获取到锁;
--tryAcquire(arg)返回false,则会执行addWaiter(Node.EXCLUSIVE)进行入队操作,由于ReentrantLock属于独占锁,因此结点类型为Node.EXCLUSIVE
public final void acquire(int arg) { //再次尝试获取同步状态
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
5)addWaiter
--创建Node:
创建了一个Node.EXCLUSIVE类型Node结点用于封装线程及其相关信息
--tail:其中,tail是AQS的成员变量,指向队尾(这点前面的我们分析过AQS维持的是一个双向的链表结构同步队列);
-> 如果是第一个结点,则为tail肯定为空,那么将执行enq(node)操作,如果非第一个结点即tail指向不为null,直接尝试执行CAS操作加入队尾,如果CAS操作失败还是会执行enq(node):
private Node addWaiter(Node mode) {
//将请求同步状态失败的线程封装成结点
Node node = new Node(Thread.currentThread(), mode);
Node pred = tail;
//如果是第一个结点加入肯定为空,跳过。
//如果非第一个结点则直接执行CAS入队操作,尝试在尾部快速添加
if (pred != null) {
node.prev = pred;
//使用CAS执行尾部结点替换,尝试在尾部快速添加
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
//如果第一次加入或者CAS操作没有成功执行enq入队操作
enq(node);
return node;
}
6)enq(node)
--死循环:使用一个死循环进行CAS操作,可以解决多线程并发问题。
--做了两件事
① 如果还没有初始同步队列则创建新结点并使用compareAndSetHead设置头结点,tail也指向head;
② 队列已存在,则将新结点node添加到队尾。
注意:这两个步骤都存在同一时间多个线程操作的可能,如果有一个线程修改head和tail成功,那么其他线程将继续循环,直到修改成功,这里使用CAS原子操作进行头结点设置和尾结点tail替换可以保证线程安全,从这里也可以看出head结点本身不存在任何数据,它只是作为一个牵头结点,而tail永远指向尾部结点(前提是队列不为null)。
private Node enq(final Node node) {
for (;;) { //死循环
Node t = tail;
//如果队列为null,即没有头结点
if (t == null) { // Must initialize
//创建并使用CAS设置头结点
if (compareAndSetHead(new Node()))
tail = head;
} else {//队尾添加新结点
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
} }}}
7)再看acquire()->acquireQueued()
--添加到同步队列后,结点就会进入一个自旋过程,即每个结点都在观察时机待条件满足获取同步状态,然后从同步队列退出并结束自旋;
--回到之前的acquire()方法,自旋过程是在acquireQueued(addWaiter(Node.EXCLUSIVE), arg))方法中执行的;
--自旋过程:
---当前线程在自旋(死循环)中获取同步状态,
---当且仅当前驱结点为头结点才尝试获取同步状态,这符合FIFO的规则,即先进先出,其次head是当前获取同步状态的线程结点,只有当head释放同步状态唤醒后继结点,后继结点才有可能获取到同步状态,因此后继结点在其前继结点为head时,才进行尝试获取同步状态,其他时刻将被挂起。
---进入if语句后调用setHead(node)方法,将当前线程结点设置为head
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) { //自旋,死循环
final Node p = node.predecessor(); //获取前驱结点
// 1 当且仅当p为头结点才尝试获取同步状态
if (p == head && tryAcquire(arg)) {
setHead(node); //将node设置为头结点
p.next = null; //清空原来头结点的引用便于GC
failed = false;
return interrupted;
}
//2 如果前驱结点不是head,判断是否挂起线程
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node); //最终都没能获取同步状态,结束该线程的请求
}
}
8)setHead(node)
--设置为node结点被设置为head后,其thread信息和前驱结点将被清空,因为该线程已获取到同步状态(锁),正在执行了,也就没有必要存储相关信息了,head只有保存指向后继结点的指针即可;
--便于head结点释放同步状态后唤醒后继结点,执行结果如下图
//设置为头结点
private void setHead(Node node) {
head = node;
//清空结点数据
node.thread = null;
node.prev = null;
}
--从图可知更新head结点的指向,将后继结点的线程唤醒并获取同步状态,调用setHead(node)将其替换为head结点,清除相关无用数据
9)shouldParkAfterFailedAcquire()
--如果前驱结点不是head执行shouldParkAfterFailedAcquire()方法
--作用:判断当前结点的前驱结点是否为SIGNAL状态(即等待唤醒状态),如果是则返回true。
如果结点的ws为CANCELLED状态(值为1>0),即结束状态,则说明该前驱结点已没有用应该从同步队列移除,执行while循环,直到寻找到非CANCELLED状态的结点。
倘若前驱结点的ws值不为CANCELLED,也不为SIGNAL(当从Condition的条件等待队列转移到同步队列时,结点状态为CONDITION因此需要转换为SIGNAL),那么将其转换为SIGNAL状态,等待被唤醒。
--shouldParkAfterFailedAcquire()方法返回true:
即前驱结点为SIGNAL状态同时又不是head结点,那么使用parkAndCheckInterrupt()方法挂起当前线程,称为WAITING状态,需要等待一个unpark()操作来唤醒它,到此ReetrantLock内部间接通过AQS的FIFO的同步队列就完成了lock()操作。
//如果前驱结点不是head,判断是否挂起线程
if (shouldParkAfterFailedAcquire(p, node) &&parkAndCheckInterrupt())
interrupted = true;
}
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
//获取当前结点的等待状态
int ws = pred.waitStatus;
//如果为等待唤醒(SIGNAL)状态则返回true
if (ws == Node.SIGNAL)
return true;
//如果ws>0 则说明是结束状态,
//遍历前驱结点直到找到没有结束状态的结点
if (ws > 0) {
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
//如果ws小于0又不是SIGNAL状态,
//则将其设置为SIGNAL状态,代表该结点的线程正在等待唤醒。
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
private final boolean parkAndCheckInterrupt() {
//将当前线程挂起
LockSupport.park(this);
//获取线程中断状态,interrupted()是判断当前中断状态,
//并非中断线程,因此可能true也可能false,并返回
return Thread.interrupted();
}
--总结成逻辑流程图:
2 ReetrantLock中非公平锁-可中断lock
--获取锁的操作,这里看看另外一种可中断的获取方式,即调用ReentrantLock类的lockInterruptibly()或者tryLock()方法,最终它们都间接调用到doAcquireInterruptibly()
1)doAcquireInterruptibly()
private void doAcquireInterruptibly(int arg)
throws InterruptedException {
final Node node = addWaiter(Node.EXCLUSIVE);
boolean failed = true;
try {
for (;;) {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return;
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
//直接抛异常,中断线程的同步状态请求
throw new InterruptedException();
}
} finally {
if (failed)
cancelAcquire(node);
}
}
--最大的不同是:
--检测到线程的中断操作后,直接抛出异常,从而中断线程的同步状态请求,移除同步队列。
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
//直接抛异常,中断线程的同步状态请求
throw new InterruptedException();
3 ReetrantLock中非公平锁-unlock()
1)release(1)
--释放锁实现:
释放同步状态的操作相对简单些,tryRelease(int releases)方法是ReentrantLock类中内部类自己实现的,因为AQS对于释放锁并没有提供具体实现,必须由子类自己实现。
--唤醒:
释放同步状态后会使用unparkSuccessor(h)唤醒后继结点的线程;
public void unlock() { //ReentrantLock类的unlock
sync.release(1);
}
public final boolean release(int arg) { //AQS类的release()方法
if (tryRelease(arg)) { //尝试释放锁
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h); //唤醒后继结点的线程
return true;
}
return false;
}
//ReentrantLock类中的内部类Sync实现的tryRelease(int releases)
protected final boolean tryRelease(int releases) {
int c = getState() - releases;
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) { //判断状态是否为0,如果是则说明已释放同步状态
free = true;
setExclusiveOwnerThread(null); //设置Owner为null
}
setState(c); //设置更新同步状态
return free;
}
2)unparkSuccessor(h)
--作用:用unpark()唤醒同步队列中最前边未放弃线程(也就是状态为CANCELLED的线程结点s)。
--前面acquireQueued():进入自旋的函数acquireQueued(),s结点的线程被唤醒后,会进入acquireQueued()函数的if (p == head && tryAcquire(arg))的判断,如果p!=head也不会有影响,因为它会执行shouldParkAfterFailedAcquire(),由于s通过unparkSuccessor()操作后已是同步队列中最前边未放弃的线程结点,那么通过shouldParkAfterFailedAcquire()内部对结点状态的调整,s也必然会成为head的next结点,因此再次自旋时p==head就成立了,然后s把自己设置成head结点,表示自己已经获取到资源了,最终acquire()也返回了,这就是独占锁释放的过程。
private void unparkSuccessor(Node node) {
//这里,node一般为当前线程所在的结点。
int ws = node.waitStatus;
if (ws < 0) //置零当前线程所在的结点状态,允许失败。
compareAndSetWaitStatus(node, ws, 0);
Node s = node.next; //找到下一个需要唤醒的结点s
if (s == null || s.waitStatus > 0) {//如果为空或已取消
s = null;
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0) //从这里可以看出,<=0的结点,都是还有效的结点。
s = t;
}
if (s != null)
LockSupport.unpark(s.thread); //唤醒
}
--总结:
在AQS同步器中维护着一个同步队列,当线程获取同步状态失败后,将会被封装成Node结点,加入到同步队列中并进行自旋操作,当当前线程结点的前驱结点为head时,将尝试获取同步状态,获取成功将自己设置为head结点。在释放同步状态时,则通过调用子类(ReetrantLock中的Sync内部类)的tryRelease(int releases)方法释放同步状态,释放成功则唤醒后继结点的线程。
4 ReetrantLock中公平锁
--与非公平锁不同的:
在获取锁的时,公平锁的获取顺序是完全遵循时间上的FIFO规则,也就是说先请求的线程一定会先获取锁,后来的线程肯定需要排队,这点与前面我们分析非公平锁的nonfairTryAcquire(int acquires)方法实现有锁不同,下面是公平锁中tryAcquire()方法的实现
--该方法与nonfairTryAcquire(int acquires)方法唯一的不同是在使用CAS设置尝试设置state值前,调用了hasQueuedPredecessors()判断同步队列是否存在结点,如果存在必须先执行完同步队列中结点的线程,当前线程进入等待状态。
--这就是非公平锁与公平锁最大的区别:
公平锁在线程请求到来时先会判断同步队列是否存在结点,如果存在先执行同步队列中的结点线程,当前线程将封装成node加入同步队列等待。
非公平锁,当线程请求到来时,不管同步队列是否存在线程结点,直接尝试获取同步状态,获取成功直接访问共享资源。
注意:在绝大多数情况下,非公平锁才是我们理想的选择,毕竟从效率上来说非公平锁总是胜于公平锁。
//公平锁FairSync类中的实现
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
//注意!!这里先判断同步队列是否存在结点
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
例如,实现一个阻塞队列,就需要两个条件变量。
一个阻塞队列,需要两个条件变量,一个是队列不空(空队列不允许出队),另一个是队列不满(队列已满不允许入队)。相关的代码:
public class BlockedQueue<T>{
final Lock lock = new ReentrantLock();
// 条件变量:队列不满
final Condition notFull = lock.newCondition();
// 条件变量:队列不空
final Condition notEmpty = lock.newCondition();
// 入队
void enq(T x) {
lock.lock();
try {
while (队列已满){
// 等待队列不满
notFull.await();
}
// 省略入队操作...
// 入队后, 通知可出队
notEmpty.signal();
}finally {
lock.unlock();
}
}
// 出队
void deq(){
lock.lock();
try {
while (队列已空){
// 等待队列不空
notEmpty.await();
}
// 省略出队操作...
// 出队后,通知可入队
notFull.signal();
}finally {
lock.unlock();
}
}
}
ReadWriteLock是一个接口,它的实现类是ReentrantReadWriteLock;
读写锁与互斥锁一个重要区别:
读写锁允许多个线程同时读共享变量,而互斥锁是不允许的,这是读写锁在读多写少场景下性能优于互斥锁的关键。但读写锁的写操作是互斥的,当一个线程在写共享变量的时候,是不允许其他线程执行写操作和读操作。
可以多个读,只能一个写
// 线程在高内聚低耦合下操纵资源类
class MyCache{ // 资源类
private volatile Map<String,Object> map = new HashMap<>();
//实现ReadWriteLock接口(不是Lock的实现类) Lock,只有一个线程
// private Lock lock = new ReentrantLock();
//要求写的时候一个线程进去,读的时候多个线程
private ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
public void put(String key,Object value){
rwLock.writeLock().lock();
try {
System.out.println(Thread.currentThread().getName() + "\t正在写入:" + key);
//模拟网络延迟
try {
TimeUnit.MILLISECONDS.sleep(300);
} catch (InterruptedException e) {
e.printStackTrace();
}
map.put(key,value);
System.out.println(Thread.currentThread().getName() + "\t写入完成!");
}catch (Exception e){
e.printStackTrace();
}finally {
rwLock.writeLock().unlock();
}
}
public void get(String key){
rwLock.readLock().lock();
try {
System.out.println(Thread.currentThread().getName() + "\t正在读取:");
//模拟网络延迟
try {
TimeUnit.MILLISECONDS.sleep(300);
} catch (InterruptedException e) {
e.printStackTrace();
}
Object result = map.get(key);
System.out.println(Thread.currentThread().getName() + "\t读取完成:" + result);
} catch (Exception e) {
e.printStackTrace();
} finally {
rwLock.readLock().unlock();
}
}
}
/**
* 多个线程同时读一个资源类没有问题,所以为了满足并发量,读取共享资源应该可以同时进行。
* 但是
* 如果有一个线程想去写共享资源,就不能再有其他线程可以对该资源进行读或写
* 小总结:
* 读-读 能共存
* 读-写 不能共存
* 写-写 不能共存
*
* 写操作:原子+独占,整个过程必须是一个完整的统一体,中间不允许被分割被打断。
*/
public class ReadWriteLockDemo {
public static void main(String[] args) {
MyCache myCache = new MyCache();
for (int i = 1; i <= 5; i++) {
System.out.println("W-"+i);
final int tempInt = i;
new Thread(()->{
myCache.put(String.valueOf(tempInt),String.valueOf(tempInt));
},"W-"+String.valueOf(i)).start();
}
for (int i = 1; i <= 5; i++) {
final int tempInt = i;
new Thread(()->{
myCache.get(String.valueOf(tempInt));
},"R-"+String.valueOf(i)).start();
}
}
}
用于控制资源能够被并发访问的线程数量/可以允许多个线程访问一个临界区;
控制并发量(阻塞队列方案)
https://blog.csdn.net/manzhizhen/article/details/81413014 √
https://blog.csdn.net/qq_36468243/article/details/86622942
说说倒计时器(CountDownLatch)和循环栅栏(CyclicBarrier)的区别
-- CountDownLatch 强调一个线程等多个线程完成某件事情。一般用于某个线程 A 等待若干个其他线程执行完任务之后,它才执行;而 CyclicBarrier 是多个线程互等,等大家都完成,再携手共进。一般用于一组线程互相等待至某个状态,然后这一组线程再同时执行;
--调用 CountDownLatch 的 countDown,当前线程并不会阻塞,会继续往下执行;而调用 CyclicBarrier 的 await 方法,会阻塞当前线程,直到 CyclicBarrier 指定的线程全部都到达了指定点的时候,才能继续往下执行;
--CountDownLatch 0时释放所有等待的线程,计数为0时,无法重置,不可重复利用。CyclicBarrier 是可以复用的,reset()方法重置屏障点,计数器会归零,重新开始计数。
https://aalion.github.io/2019/12/28/concurrency81/
---------------具体----------------
1 CountDownLatch->倒计时器
--使用场景:在多线程协作完成业务功能时,有时候需要等待其他多个线程完成任务之后,主线程才能继续往下执行业务功能。例如,在主线程中启动10个子线程去数据库中获取分页数据,需要等到所有线程数据都返回之后统一做统计处理
--例子:
6人运动员跑步比赛,裁判员在终点计时,可以想象每当一个运动员到达终点的时候,对于裁判员来说就少了一个计时任务。直到所有运动员都到达终点了,裁判员的任务也才完成。
这 6 个运动员可以类比成 6 个线程,当线程调用 CountDownLatch.countDown 方法时就会对计数器的值减一,直到计数器的值为 0 的时候,裁判员(调用 await 方法的线程)才能继续往下执行。
public class D81_CountDownLatchDemo{
private static CountDownLatch startSingnal=new CountDownLatch(1);//构造方法
//用来表示裁判员需要维护的是6个运动员
public static CountDownLatch endSingnal=new CountDownLatch(6);//构造方法
public static void main (String[] args) throws InterruptedException{
// 创建一个固定大小的线程池
ExecutorService executorService= Executors.newFixedThreadPool(6);
for (int i = 0; i <6; i++) {
executorService.execute(()->{
try {
System.out.println(Thread.currentThread().getName()+"运动员等待裁判响哨~");
startSingnal.await();//等到构造方法传入的 N 减到 0 的时候,当前调用await方法的线程继续执行
System.out.println(Thread.currentThread().getName()+"正在冲刺~");
endSingnal.countDown();//使 CountDownLatch 值 N 减 1
System.out.println(Thread.currentThread().getName()+"到达终点~");
} catch (InterruptedException e) {
e.printStackTrace();
}
});
}
Thread.sleep(1000);
System.out.println("裁判发令~");
startSingnal.countDown();
endSingnal.await();
System.out.println("全部到达终点,比赛结束~");
executorService.shutdown();
}
}
//输出
pool-1-thread-1运动员等待裁判响哨~
pool-1-thread-5运动员等待裁判响哨~
pool-1-thread-3运动员等待裁判响哨~
pool-1-thread-2运动员等待裁判响哨~
pool-1-thread-6运动员等待裁判响哨~
pool-1-thread-4运动员等待裁判响哨~
裁判发令~
pool-1-thread-1正在冲刺~
pool-1-thread-1到达终点~
pool-1-thread-5正在冲刺~
pool-1-thread-5到达终点~
pool-1-thread-3正在冲刺~
pool-1-thread-3到达终点~
pool-1-thread-2正在冲刺~
pool-1-thread-2到达终点~
pool-1-thread-6正在冲刺~
pool-1-thread-6到达终点~
pool-1-thread-4正在冲刺~
pool-1-thread-4到达终点~
全部到达终点,比赛结束~
2CyclicBarrier->循环栅栏
--例子
开运动会时,会有跑步这一项运动,我们来模拟下运动员入场时的情况,假设有 6 条跑道,在比赛开始时,就需要6个运动员在比赛开始的时候都站在起点了,裁判员吹哨后才能开始跑步。跑道起点就相当于“barrier”,是临界点,而这6个运动员就类比成线程的话,就是这 6 个线程都必须到达指定点了,意味着凑齐了一波,然后才能继续执行,否则每个线程都得阻塞等待,直至凑齐一波即可。cyclic 是循环的意思,也就是说 CyclicBarrier 当多个线程凑齐了一波之后,仍然有效,可以继续凑齐下一波。
public class CyclicBarrierDemo {
//指定必须有6个运动员到达才行,构造方法public CyclicBarrier(int parties, Runnable barrierAction)
private static CyclicBarrier barrier = new CyclicBarrier(6, () -> {
System.out.println("所有运动员已入场,裁判吹起跑哨~");
});
public static void main(String[] args) {
System.out.println("运动员准备入场,欢呼~");
ExecutorService service = Executors.newFixedThreadPool(6);
for (int i = 0; i < 6; i++) {
service.execute(() -> {
try {
System.out.println(Thread.currentThread().getName() + "运动员,进场");
barrier.await();//等到所有的线程都到达指定的临界点:6人到齐
System.out.println(Thread.currentThread().getName() + "运动员出发~");
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
});
}
}
}
ArrayList是线程不安全的,例子以及解决方案(sxt2)
psvm{
List<String> list = new ArrayList<>();
for(int i=1; i<30; i++){
new Thread(() -> {
list.aa(UUID.randomUUID().toString().substring(0,8));
sout(list);
},String.valueOf(i)).start();
}
}
① 故障现象:java.util.ConcurrentModificationException(并发修改异常)
② 导致原因:并发争抢修改导致,一个正在写,另一个线程过来抢夺,导致数据不一致异常。并发生修改异常。
③ 解决方案:
④ 优化建议(同样的错误不犯第2次)
2)解决非线程安全
方案1:使用Vertor集合
缺点:Vertor加锁可以保证数据一致性,但并发性低
new Vector<>();
方案2:使用Collections.synchronizedList
Collection:集合接口
Collections:集合接口辅助类
缺点:
Collections.synchronizedList(new ArrayList<>());
方案3:使用JUC中的CopyOnWriteArrayList类替换。
new CopyOnWriteArraylist<>();
3)实现-Collections.synchronizedList实现
初始化
ArrayList arrayList = new ArrayList();
List list2 = Collections.synchronizedList(arrayList);
add方法:通过关键字synchronized同步
public void add(int index, E element) {
synchronized (mutex) {list.add(index, element);}
}
get方法:synchronized
public V get(Object key) {
synchronized (mutex) {return m.get(key);}
}
实现-②CopyonwriteArrayList(写时复制,读写分离的思想)
Arrays.copyOf,扩容长度+1
/** The lock protecting all mutators */
transient final ReentrantLock lock = new ReentrantLock();
/** The array, accessed only via getArray/setArray. */
private volatile transient Object[] array;//保证了线程的可见性
public boolean add(E e) {
final ReentrantLock lock = this.lock;//ReentrantLock 保证了线程的可见性和顺序性,即保证了多线程安全。// 获取独占锁
lock.lock();
try {
Object[] elements = getArray();
int len = elements.length;
Object[] newElements = Arrays.copyOf(elements, len + 1);//在原先数组基础之上新建长度+1的数组,并将原先数组当中的内容拷贝到新数组当中。
newElements[len] = e;//设值
setArray(newElements);//对新数组进行赋值
return true;
} finally {
lock.unlock();
}
}
get:无锁
public E get(int index) {
return get(getArray(), index);
}
CopyOnWriteArrayList,咋实现线程安全的?。
Collections.synchronizedList和CopyOnWriteArrayList的异同点?
1)同: 实现线程安全的列表方式
2)异:
CopyOnWriteArrayList写的时候读会读到空数据吗?
ConcurrentHashmap和Hashtable
【ConcurrentHashmap和Hashtable都是支持并发的,这样会有一个问题,当你通过get(k)获取对应的value时,如果获取到的是null时,你无法判断,它是put(k,v)的时候value为null,还是这个key从来没有做过映射。HashMap是非并发的,可以通过contains(key)来做这个判断。而支持并发的Map在调用m.contains(key)和m.get(key),m可能已经不同了。】
底层数据结构改变为采用数组+链表+红黑树的数据形式。
https://juejin.im/post/5aeeaba8f265da0b9d781d16
Set非线程安全
1) 线程安全问题
故障现象:java.util.ConcurrentModificationException(并发修改异常)
Collections.syschronizedSet(new hashSet<>());
new CopyOnWriteArraySet<>(); //底层还是CopyOnWriteArrayList()方法实现的
2 原理
1) 使用
--线程进来之后初始化一个可以泛型的ThreadLocal对象,之后这个线程只要在remove之前去get,都能拿到之前set的值,注意这里我说的是remove之前.
ThreadLocal<String> localName = new ThreadLocal();
localName.set("张三");
String name = localName.get();
localName.remove();
2)set源码
public void set(T value) {
Thread t = Thread.currentThread(); // 获取当前线程
ThreadLocalMap map = getMap(t); // 获取ThreadLocalMap对象
if (map != null) // 校验对象是否为空
map.set(this, value); // 不为空set
else
createMap(t, value); // 为空创建一个map对象
}
--ThreadLocalMap是当前线程Thread一个叫threadLocals的变量中获取的。
--隔离的实现:每个线程Thread都维护了自己的threadLocals变量,所以在每个线程创建ThreadLocal的时候,实际上数据是存在自己线程Thread的threadLocals变量里面的,别人没办法拿到,从而实现了隔离。
// 代码段1
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
// 代码段2
public class Thread implements Runnable {
……
ThreadLocal.ThreadLocalMap threadLocals = null;
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
……
3)ThreadLocalMap底层结构
详见后面;
7)对象的存放位置
--Java中对象存储:
栈内存归属于单个线程,每个线程都会有一个栈内存,其存储的变量只能在其所属线程中可见,即栈内存可以理解成线程的私有内存,而堆内存中的对象对所有线程可见,堆内存中的对象可以被所有线程访问。
--threadlocal示例及值:堆
ThreadLocal实例实际上也是被其创建的类持有(更顶端应该是被线程持有),而ThreadLocal的值其实也是被线程实例持有,它们都是位于堆上,只是通过一些技巧将可见性修改成了线程可见。
8)线程共享ThreadLocal的数据
--InheritableThreadLocal类:
实现多个线程访问ThreadLocal的值,在主线程中创建一个InheritableThreadLocal的实例,然后在子线程中得到这个InheritableThreadLocal实例设置的值。
--测试输出:
在子线程中我是能够正常输出那一行日志的,这也是我之前面试视频提到过的父子线程数据传递的问题。
private void test() {
final ThreadLocal threadLocal = new InheritableThreadLocal();
threadLocal.set("帅得一匹");
Thread t = new Thread() {
@Override
public void run() {
super.run();
Log.i( "张三帅么 =" + threadLocal.get());
}
};
t.start();
}
--父子线程数据传递:
① inheritableThreadLocals变量:
public class Thread implements Runnable {
……
ThreadLocal.ThreadLocalMap threadLocals = null;
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
……
②Thread.init初始化创建源码:
--线程的inheritThreadLocals变量不为空,如上面的例子,且父线程的inheritThreadLocals也存在,那么就把父线程的inheritThreadLocals给当前线程的inheritThreadLocals。(或者说是复制)
public class Thread implements Runnable {
……
if (inheritThreadLocals && parent.inheritableThreadLocals != null)
this.inheritableThreadLocals=ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
……
}
9)内存泄露
--ThreadLocal在保存的时候会把自己当做Key存在ThreadLocalMap中;
--正常情况应该是key和value都应该被外界强引用才对,但现在key被设计成WeakReference弱引用。
--弱引用:只能存活到下一次GC前
--概念:
①Memory overflow:内存溢出,没有足够的内存提供申请者使用。
②Memory leak:内存泄漏,程序申请内存后,无法释放已申请的内存空间,内存泄漏的堆积终将导致内存溢出。
--内存泄漏问题:
ThreadLocal在没有外部强引用时,发生GC时key会被回收,如果创建ThreadLocal的线程一直持续运行,那么这个Entry对象中的value就有可能一直得不到回收,发生内存泄露。
一般一个线程使用完,ThreadLocalMap是应该要被清空的,但是现在线程被复用
public class ThreadLocal<T> {
static class ThreadLocalMap {
static class Entry extends WeakReference<ThreadLocal<?>> {
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
.....
--内存泄漏解决:
在使用的最后用remove把值清空;
remove的源码很简单,找到对应的值全部置空,这样在垃圾回收器回收的时候,会自动把他们回收掉。
ThreadLocal<String> localName = new ThreadLocal();
try {
localName.set("张三");
……
} finally {
localName.remove();
}
--key设计成弱引用的原因:
key不设置成弱引用的话就会造成和entry中value一样内存泄漏的场景。
另,ThreadLocal的不足,可以通过看看netty的fastThreadLocal来弥补
3 使用场景
1)Spring实现事务隔离级别的源码
--Spring采用Threadlocal的方式,来保证单个线程中的数据库操作使用的是同一个数据库连接,同时,采用这种方式可以使业务层使用事务时不需要感知并管理connection对象,通过传播级别,巧妙地管理多个事务配置之间的切换,挂起和恢复。
--Spring框架里面就是用的ThreadLocal来实现这种隔离,主要是在TransactionSynchronizationManager这个类里面,代码如下所示:
(注:Spring的事务主要是ThreadLocal和AOP去做实现)
private static final Log logger = LogFactory.getLog(TransactionSynchronizationManager.class);
private static final ThreadLocal<Map<Object, Object>> resources =
new NamedThreadLocal<>("Transactional resources");
private static final ThreadLocal<Set<TransactionSynchronization>> synchronizations =
new NamedThreadLocal<>("Transaction synchronizations");
private static final ThreadLocal<String> currentTransactionName =
new NamedThreadLocal<>("Current transaction name");
……
1 ThreadLocalMap底层结构
--数据结构很像HashMap,但看源码并未实现Map接口;
--其Entry是继承WeakReference(弱引用)的,也没有HashMap中的next,所以也不存在链表。
static class ThreadLocalMap {
static class Entry extends WeakReference<ThreadLocal<?>> {
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
……
}
1)使用数组
--因为,开发过程中可以一个线程可以有多个TreadLocal来存放不同类型的对象的,但是他们都将放到你当前线程的ThreadLocalMap里,所以肯定要数组来存。
2 hash算法
1)hash算法:
int i = key.threadLocalHashCode & (len-1); //下标位置
2)斐波那契数/黄金分割数:
HASH_INCREMENT = 0x61c88647;
--hash计算因子;
--每当创建一个ThreadLocal对象,ThreadLocal.nextHashCode 这个值就会增长 0x61c88647;
--hash增量为 这个数字,带来的好处就是 hash 分布非常均匀。
public class ThreadLocal<T> {
// 哈希函数
private final int threadLocalHashCode = nextHashCode(); //②
private static AtomicInteger nextHashCode = new AtomicInteger();
private static final int HASH_INCREMENT = 0x61c88647;
private static int nextHashCode() { // ③
return nextHashCode.getAndAdd(HASH_INCREMENT);
}
static class ThreadLocalMap {
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
table = new Entry[INITIAL_CAPACITY];
// i就是当前key在散列表中对应的数组下标位置。
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1); // ①
table[i] = new Entry(firstKey, firstValue);
size = 1;
setThreshold(INITIAL_CAPACITY);
}
}
}
4 ThreadLocalMap.set()
set数据(新增或者更新数据)好几种hash情况:(图的方式解析了set()实现的原理)
1)情况一: 通过hash计算后的槽位对应的Entry数据为空:
--直接将数据放到该槽位:
2)情况二: 槽位数据不为空,key值与当前ThreadLocal通过hash计算获取的key值一致:
--直接更新该槽位的数据;
3)情况三: 槽位数据不为空,往后遍历过程中,在找到Entry为null的槽位之前,没有遇到key过期的Entry:
--遍历散列数组,线性往后查找:
① 如果找到Entry为null的槽位,则将数据放入该槽位中,
② 如果往后遍历过程中,遇到了key值相等的数据,直接更新即可。
4)情况四:槽位数据不为空,往后遍历,找到Entry为null的槽位之前,遇到key过期的Entry:
第一步:
--往后遍历过程中,到了index=7的槽位数据Entry的key=null:
1> 情况4.1:向后遍历过程,找到相同key值的Entry数据:
第二步
--此时就会执行replaceStaleEntry()方法,该方法含义是替换过期数据的逻辑,以index=7位起点开始遍历,进行探测式数据清理工作。
--初始化探测式清理过期数据扫描的开始位置:slotToExpunge = staleSlot = 7
--以当前staleSlot开始 向前迭代查找,找其他过期的数据,然后更新过期数据起始扫描下标slotToExpunge。for循环迭代,直到碰到Entry为null结束。
--如果找到了过期的数据,继续向前迭代,直到遇到Entry=null的槽位才停止迭代,如下图所示,slotToExpunge被更新为0:
--以当前节点(index=7)向前迭代,检测是否有过期的Entry数据,如果有则更新slotToExpunge值。碰到null则结束探测。以上图为例slotToExpunge被更新为0。
--上面向前迭代的操作是为了更新探测清理过期数据的起始下标slotToExpunge(15前向)的值,这个值在后面会讲解,它是用来判断当前过期槽位staleSlot之前是否还有过期元素。
第三步:
--接着开始以staleSlot位置(index=7)向后迭代,如果找到了相同key值的Entry数据:
tips:23
第四步:
--从当前节点staleSlot(7)向后查找key值相等的Entry元素,找到相同的(23)后更新Entry的值(27)并交换staleSlot元素的位置(27的值交换到7)(staleSlot位置为过期元素 7),更新Entry数据,然后开始进行过期Entry的清理工作,如下图所示:
1> 情况4.2:向后遍历过程中,如果没有找到相同key值的Entry数据:
第三步:
--从当前节点staleSlot(7)向后查找key值相等的Entry元素,直到Entry为null则停止寻找。通过上图可知,此时table中没有key值相同的Entry。
第四步:
创建新的Entry,替换table[stableSlot]位置:
清理:替换完成后也是进行过期元素清理工作,清理工作主要是有两个方法:expungeStaleEntry()和cleanSomeSlots()
5 set()源码
private void set(ThreadLocal<?> key, Object value) {
Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len-1);
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
`ThreadLocal`<?> k = e.get();
if (k == key) {
e.value = value;
return;
}
if (k == null) { //无效entry
replaceStaleEntry(key, value, i);
return;
}
}
tab[i] = new Entry(key, value);
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
1)下标
--计算:通过key来计算在散列表中的对应位置;
--使用:以当前key对应的桶的位置向后查找,找到可以使用的桶。
--可用的桶:
① k = key 说明是相等替换操作,可以使用
② 碰到一个过期的桶,执行替换逻辑,占用过期桶
③ 查找过程中,碰到桶中Entry=null的情况,直接使用
Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len-1);
2)nextIndex()-遍历
--向后查找、向前查找
private static int nextIndex(int i, int len) {
return ((i + 1 < len) ? i + 1 : 0);
}
private static int prevIndex(int i, int len) {
return ((i - 1 >= 0) ? i - 1 : len - 1);
}
3)for循环
--如果key值对应的桶中Entry数据不为空
① 如果k = key,替换后直接返回
② 如果key = null,当前桶位置的Entry是过期数据,执行replaceStaleEntry()方法(核心方法),然后返回
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
`ThreadLocal`<?> k = e.get();
if (k == key) {
e.value = value;
return;
}
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
3.1)replaceStaleEntry()--提供替换过期数据的功能
private void replaceStaleEntry(ThreadLocal<?> key, Object value,
int staleSlot) {
Entry[] tab = table;
int len = tab.length;
Entry e;
int slotToExpunge = staleSlot;
for (int i = prevIndex(staleSlot, len);
(e = tab[i]) != null;
i = prevIndex(i, len))
if (e.get() == null)
slotToExpunge = i;
for (int i = nextIndex(staleSlot, len);
(e = tab[i]) != null;
i = nextIndex(i, len)) {
`ThreadLocal`<?> k = e.get();
if (k == key) {
e.value = value;
tab[i] = tab[staleSlot];
tab[staleSlot] = e;
if (slotToExpunge == staleSlot)
slotToExpunge = i;
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
return;
}
if (k == null && slotToExpunge == staleSlot)
slotToExpunge = i;
}
tab[staleSlot].value = null;
tab[staleSlot] = new Entry(key, value);
if (slotToExpunge != staleSlot)
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
}
1> 向前迭代 (向前,找到无效就记录下标)
--slotToExpunge:开始探测式清理过期数据的开始下标,默认从当前的staleSlot开始。
--for流程:
for循环一直碰到Entry为null结束;
当前staleSlot开始,向前迭代,找过期数据;
若找到过期数据,更新探测清理过期数据的开始下标为i,即slotToExpunge=i
for (int i = prevIndex(staleSlot, len);
(e = tab[i]) != null;
i = prevIndex(i, len)){
if (e.get() == null){
slotToExpunge = i;
}
}
2>向后迭代
--for流程:k==key (在找到null之前,先找到相同,替换,记录i)
-> 从staleSlot向后查找,也是碰到Entry为null的桶结束;
-> k==key,替换逻辑,替换新数据并且交换当前staleSlot位置;
此时,若slotToExpunge == staleSlot,这说明replaceStaleEntry()一开始向前查找过期数据时并未找到过期的Entry数据,接着向后查找过程中也未发现过期数据,修改开始探测式清理过期数据的下标为当前循环的index,即slotToExpunge = i。
-> 最后调用cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);进行启发式过期数据清理。
for(){
if (k == key) {
e.value = value;
tab[i] = tab[staleSlot];
tab[staleSlot] = e;
if (slotToExpunge == staleSlot)
slotToExpunge = i;
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
return;
}
}
--for流程:k!=key (在找到null之前,不同,再找到无效,记录i)
-> k != key则会接着往下走:
k==null:当前遍历的Entry是一个过期数据;
slotToExpunge == staleSlot:一开始的向前查找数据并未找到过期的Entry。
-> 上2条件成立,则更新slotToExpunge为i,这个前提是前驱节点扫描时未发现过期数据。
for(){
if (k == key) {...}
if (k == null && slotToExpunge == staleSlot)
slotToExpunge = i;
}
3>添加逻辑 (在找到null时,也未找到相同或无效,直接赋值)
--往后迭代的过程中如果没有找到k == key的数据,且碰到Entry为null的数据,则结束当前的迭代操作。
--说明:这里是一个添加的逻辑,将新的数据添加到table[staleSlot] 对应的slot中。
tab[staleSlot].value = null;
tab[staleSlot] = new Entry(key, value);
4>清理数据
除了staleSlot以外,还发现了其他过期的slot数据,就要开启清理数据的逻辑:
if (slotToExpunge != staleSlot)
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
4)for结束
--for结束:说明向后迭代的过程中遇到了entry为null的情况:
-> 在Entry为null的桶中创建一个新的Entry对象
-> 执行++size操作
->启发式:调用cleanSomeSlots()做一次启发式清理工作,清理散列数组中Entry的key过期的数据
--->如果清理工作完成后,未清理到任何数据,且size超过了阈值(数组长度的2/3),进行rehash()操作
----->探测清理:rehash()中会先进行一轮探测式清理,清理过期key,清理完成后如果size >= threshold - threshold / 4,就会执行真正的扩容逻辑(扩容逻辑往后看)
tab[i] = new Entry(key, value);
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
//---------------------------------
private void rehash() {
expungeStaleEntries();
// Use lower threshold for doubling to avoid hysteresis
if (size >= threshold - threshold / 4)
resize();
}
5)过期清理
ThreadLocalMap的两种过期key数据清理方式:探测式清理和启发式清理。
1>探测式清理:
--基本思路:
探测式清理,也就是expungeStaleEntry方法,遍历散列数组,从开始位置向后探测清理过期数据,将过期数据的Entry设置为null,沿途中碰到未过期的数据则将此数据rehash后重新在table数组中定位,如果定位的位置已经有了数据,则会将未过期的数据放到最靠近此位置的Entry=null的桶中,使rehash后的Entry数据距离正确的桶的位置更近一些。
--正常数据处理:
往后迭代检查,碰到正常数据,计算该数据位置是否偏移,如果被偏移,则重新计算slot位置,目的是让正常数据尽可能存放在正确位置或离正确位置更近的位置
--终止:
往后迭代的过程中碰到空的槽位,终止探测,这样一轮探测式清理工作就完成了
private int expungeStaleEntry(int staleSlot) {
Entry[] tab = table;
int len = tab.length;
tab[staleSlot].value = null;
tab[staleSlot] = null;
size--;
Entry e;
int i;
for (i = nextIndex(staleSlot, len);
(e = tab[i]) != null;
i = nextIndex(i, len)) {
`ThreadLocal`<?> k = e.get();
if (k == null) {
e.value = null;
tab[i] = null;
size--;
} else {
int h = k.threadLocalHashCode & (len - 1);
if (h != i) {
tab[i] = null;
while (tab[h] != null)
h = nextIndex(h, len);
tab[h] = e;
}
}
}
return i;
}
2>启发式清理:(cleanSomeSlots())
而启发式清理被作者定义为:Heuristically scan some cells looking for stale entries.
private boolean cleanSomeSlots(int i, int n) {
boolean removed = false;
Entry[] tab = table;
int len = tab.length;
do {
i = nextIndex(i, len);
Entry e = tab[i];
if (e != null && e.get() == null) {
n = len;
removed = true;
i = expungeStaleEntry(i);
}
} while ( (n >>>= 1) != 0);
return removed;
}
6 扩容机制
1)触发
--执行完启发式清理工作后,未清理到任何数据,且当前散列数组中Entry的数量已经达到了列表的扩容阈值(len*2/3),就开始执行rehash()`逻辑:
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
2)rehash
--先是会进行探测式清理工作,从table的起始位置往后清理,流程看上面。
--清理完成之后,table中可能有一些key为null的Entry数据被清理掉,所以此时通过判断size >= threshold - threshold / 4 也就是size >= threshold* 3/4 来决定是否扩容。
private void rehash() {
expungeStaleEntries();
if (size >= threshold - threshold / 4)
resize();
}
private void expungeStaleEntries() {
Entry[] tab = table;
int len = tab.length;
for (int j = 0; j < len; j++) {
Entry e = tab[j];
if (e != null && e.get() == null)
expungeStaleEntry(j);
}
}
--注意:rehash和resize时机区别:
rehash()的阈值是size >= threshold,所以当面试官套路我们ThreadLocalMap扩容机制的时候 我们一定要说清楚这两个步骤:
3)resize()方法
--为了方便演示,我们以oldTab.len=8来举例:
--扩容后的tab的大小为oldLen * 2,然后遍历老的散列表,重新计算hash位置,然后放到新的tab数组中,如果出现hash冲突则往后寻找最近的entry为null的槽位,遍历完成之后,oldTab中所有的entry数据都已经放入到新的tab中了。重新计算tab下次扩容的阈值,具体代码如下:
private void resize() {
Entry[] oldTab = table;
int oldLen = oldTab.length;
int newLen = oldLen * 2;
Entry[] newTab = new Entry[newLen];
int count = 0;
for (int j = 0; j < oldLen; ++j) {
Entry e = oldTab[j];
if (e != null) {
`ThreadLocal`<?> k = e.get();
if (k == null) {
e.value = null;
} else {
int h = k.threadLocalHashCode & (newLen - 1);
while (newTab[h] != null)
h = nextIndex(h, newLen);
newTab[h] = e;
count++;
}
}
}
setThreshold(newLen);
size = count;
table = newTab;
}
7 get源码
--get的时,也会根据ThreadLocal对象的hash值,定位到table中的位置,然后判断该位置Entry对象中的key是否和get的key一致,如果不一致,就判断下一个位置,set和get如果冲突严重的话,效率还是很低的。
1)第一种情况: 通过查找key值计算出散列表中slot位置,然后该slot位置中的Entry.key和查找的key一致,则直接返回:
2)第二种情况: slot位置中的Entry.key和要查找的key不一致:
继续往后迭代查找;遇到Entry.key=null,触发一次探测式数据回收操作,执行expungeStaleEntry()方法,执行完后,有些数据会被回收,有些数据都会前移,此时继续往后迭代,找到key值相等的Entry数据
private Entry getEntry(ThreadLocal<?> key) {
int i = key.threadLocalHashCode & (table.length - 1);
Entry e = table[i];
if (e != null && e.get() == key)
return e;
else
return getEntryAfterMiss(key, i, e);
}
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
Entry[] tab = table;
int len = tab.length;
// get的时候一样是根据ThreadLocal获取到table的i值,然后查找数据拿到后会对比key是否相等 if (e != null && e.get() == key)。
while (e != null) {
ThreadLocal<?> k = e.get();
// 相等就直接返回,不相等就继续查找,找到相等位置。
if (k == key)
return e;
if (k == null)
expungeStaleEntry(i);
else
i = nextIndex(i, len);
e = tab[i];
}
return null;
}
---------------ab------------
5)哈希冲突
private void set(ThreadLocal<?> key, Object value) {
Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len-1);
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get();
if (k == key) {
e.value = value;
return;
}
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
tab[i] = new Entry(key, value);
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
--ThreadLocalMap在存储的时候会给每一个ThreadLocal对象一个threadLocalHashCode,在插入过程中,根据ThreadLocal对象的hash值,定位到table中的位置i,int i = key.threadLocalHashCode & (len-1)。
--情况1:
如果当前位置是空的,就初始化一个Entry对象放在位置i上;
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
--情况2:
如果位置i不为空,如果这个Entry对象的key正好是即将设置的key,那么就刷新Entry中的value;
if (k == key) {
e.value = value;
return;
}
--情况3:
如果位置i的不为空,而且key不等于entry,那就找下一个空位置,直到为空为止。
阻塞队列知道吗?
如何设计一个消息队列
消息队列的作用
使用过哪些任务队列?
1)线程池-ArrayBlockingQueue
3种线程实现的3种方式?
1)通过继承Thread类,重写run方法;
2)通过实现runable接口;
3)通过实现callable接口。(和Future)
④ 线程池获取:ThreadPoolExecutor
public class CreateThreadDemo {
public static void main(String[] args) {
//1.继承Thread
Thread thread = new Thread() {
@Override
public void run() {
System.out.println("继承Thread");
super.run();
}
};
thread.start();
//2.实现runable接口
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("实现runable接口");
}
});
thread1.start();
//3.实现callable接口
ExecutorService service = Executors.newSingleThreadExecutor();
Future<String> future = service.submit(new Callable() {
@Override
public String call() throws Exception {
return "通过实现Callable接口";
}
});
try {
String result = future.get();
System.out.println(result);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}
继承Thread和实现Runnable接口的区别,这两者的继承关系
--实现:通过继承Thread类,重写Thread的run()方法,将线程运行的逻辑放在其中;通过实现Runnable接口,实例化Thread类
--如果一个类继承Thread,则不适合资源共享。但是如果实现了Runable接口的话,则很容易的实现资源共享。继承Thread是多个线程分别完成自己的任务,实现了Runable是多个线程共同完成一个任务。
实现Runnable接口比继承Thread类所具有的优势:
1)适合多个相同的程序代码的线程去处理同一个资源
2)Java只能单继承,可以避免java中的单继承的限制
3)增加程序的健壮性,代码可以被多个线程共享,代码和数据独立,
4) 如果只想重写 run() 方法,而不重写其他 Thread 方法,那么应使用 Runnable 接口
https://www.cnblogs.com/CryOnMyShoulder/p/8028122.html
Callable和Runnable的区别?
工具类 Executors 可以实现 Runnable 对象和 Callable对象之间的相互转换。(Executors.callable(Runnable task)或 Executors.callable(Runnable task,Object resule))。
// ① Runnable.java
@FunctionalInterface
public interface Runnable {
/** 被线程执行,没有返回值也无法抛出异常*/
public abstract void run();
}
// ② Callable.java
@FunctionalInterface
public interface Callable<V> {
/**计算结果,或在无法这样做时抛出异常。
* @return 计算得出的结果 @throws 如果无法计算结果,则抛出异常*/
V call() throws Exception;
}
优劣
采用继承Thread类方式:
(1)优点:编写简单,如果需要访问当前线程,无需使用Thread.currentThread()方法,直接使用this,即可获得当前线程。
(2)缺点:因为线程类已经继承了Thread类,所以不能再继承其他的父类。
采用实现Runnable接口方式:
(1)优点:线程类只是实现了Runable接口,还可以继承其他的类。在这种方式下,可以多个线程共享同一个目标对象,所以非常适合多个相同线程来处理同一份资源的情况,从而可以将CPU代码和数据分开,形成清晰的模型,较好地体现了面向对象的思想。
(2)缺点:编程稍微复杂,如果需要访问当前线程,必须使用Thread.currentThread()方法。
https://blog.csdn.net/Touch_2011/article/details/6891026
为什么用线程池,优势 (sxt2)
JVM如何查看运行的线程数量?
谈谈你对多线程的理解?
public interface Executor { //顶级接口Executor,定义了线程执行的方法
void execute(Runnable command);
}
ExecutorService
public interface ExecutorService extends Executor {
3个submit方法的介绍:参数不同
① 提交Runnable任务 submit(Runnable task):
这个方法的参数是一个Runnable接口,Runnable接口的run()方法是没有返回值的,所以 submit(Runnable task)这个方法返回的Future仅可以用来断言任务已经结束了,类似于Thread.join()。
② 提交Callable任务 submit(Callable task):
这个方法的参数是一个Callable接口,它只有一个call()方法,并且这个方法是有返回值的,所以这个方法返回的Future对象可以通过调用其get()方法来获取任务的执行结果。
③ 提交Runnable任务及结果引用submit(Runnable task, T result):
这个方法很有意思,假设这个方法返回的Future对象是f,f.get()的返回值就是传给submit()方法的参数result。
方法③submit(Runnable task, T result)的用法?
--经典用法代码展示如下:
--注意:
Runnable接口的实现类Task声明了一个有参构造函数Task(Result r),创建Task对象的时候传入了result对象,这样就能在类Task的run()方法中对result进行各种操作了。result相当于主线程和子线程之间的桥梁,通过它主子线程可以共享数据。
ExecutorService executor = Executors.newFixedThreadPool(1);
// 创建Result对象r
Result r = new Result();
r.setAAA(a);
// 提交任务
Future<Result> future = executor.submit(new Task(r), r);
Result fr = future.get();
// 下面等式成立
fr === r;
fr.getAAA() === a;
fr.getXXX() === x
class Task implements Runnable{
Result r;
//通过构造函数传入result
Task(Result r){
this.r = r;
}
void run() {
//可以操作result
a = r.getAAA();
r.setXXX(x);
}
}
Future
public interface Future {
FutureTask
public class FutureTask implements RunnableFuture {
如何使用FutureTask?
FutureTask实现了Runnable和Future接口
① 实现了Runnable接口,可以将FutureTask对象作为任务提交给ThreadPoolExecutor去执行,也可以直接被Thread执行;
② 实现了Future接口,所以也能用来获得任务的执行结果。
示例代码①: 将FutureTask对象提交给ThreadPoolExecutor去执行。
// 创建FutureTask
FutureTask<Integer> futureTask = new FutureTask<>(()-> 1+2);
// 创建线程池
ExecutorService es = Executors.newCachedThreadPool();
// 提交FutureTask
es.submit(futureTask);
// 获取计算结果
Integer result = futureTask.get();
示例代码②:FutureTask对象直接被Thread执行的示例代码如下所示。
可以看出:利用FutureTask对象可以很容易获取子线程的执行结果。
// 创建FutureTask
FutureTask<Integer> futureTask = new FutureTask<>(()-> 1+2);
// 创建并启动线程
Thread T1 = new Thread(futureTask);
T1.start();
// 获取计算结果
Integer result = futureTask.get();
实现最优的“烧水泡茶”程序?
// 创建任务T2的FutureTask
FutureTask<String> ft2 = new FutureTask<>(new T2Task());
// 创建任务T1的FutureTask
FutureTask<String> ft1 = new FutureTask<>(new T1Task(ft2));
// 线程T1执行任务ft1
Thread T1 = new Thread(ft1);
T1.start();
// 线程T2执行任务ft2
Thread T2 = new Thread(ft2);
T2.start();
// 等待线程T1执行结果
System.out.println(ft1.get());
// T1Task需要执行的任务:
// 洗水壶、烧开水、泡茶
class T1Task implements Callable<String>{
FutureTask<String> ft2;
// T1任务需要T2任务的FutureTask
T1Task(FutureTask<String> ft2){
this.ft2 = ft2;
}
@Override
String call() throws Exception {
System.out.println("T1:洗水壶...");
TimeUnit.SECONDS.sleep(1);
System.out.println("T1:烧开水...");
TimeUnit.SECONDS.sleep(15);
String tf = ft2.get(); // ★ 获取T2线程的茶叶
System.out.println("T1:拿到茶叶:"+tf);
System.out.println("T1:泡茶...");
return "上茶:" + tf;
}
}
// T2Task需要执行的任务:
// 洗茶壶、洗茶杯、拿茶叶
class T2Task implements Callable<String>{
@Override
String call() throws Exception{
System.out.println("T2:洗茶壶...");
TimeUnit.SECONDS.sleep(1);
System.out.println("T2:洗茶杯...");
TimeUnit.SECONDS.sleep(2);
System.out.println("T2:拿茶叶...");
TimeUnit.SECONDS.sleep(1);
return "龙井";
}
}
// 一次执行结果:
T1:洗水壶...
T2:洗茶壶...
T1:烧开水...
T2:洗茶杯...
T2:拿茶叶...
T1:拿到茶叶:龙井
T1:泡茶...
上茶:龙井
线程池的参数解释
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
if (corePoolSize < 0 ||
maximumPoolSize <= 0 ||
maximumPoolSize < corePoolSize ||
keepAliveTime < 0)
throw new IllegalArgumentException();
if (workQueue == null || threadFactory == null || handler == null)
throw new NullPointerException();
this.acc = System.getSecurityManager() == null ?
null :
AccessController.getContext();
this.corePoolSize = corePoolSize;
this.maximumPoolSize = maximumPoolSize;
this.workQueue = workQueue;
this.keepAliveTime = unit.toNanos(keepAliveTime);
this.threadFactory = threadFactory;
this.handler = handler;
}
7大参数(sxt2)
1)corePoolSize:线程池中的常驻核心线程池数
2)maximumPoolSize:线程池能够容纳同时执行的最大线程数,此值必须大于等于1
3)keepAliveTime:多余的空闲线程的存活时间
当前线程池数量超过corePoolSize时,当空闲时间达到keepAliveTime值时,多余的空闲线程会被销毁直到只剩下corePoolSize个线程为止。
(注,只有当线程池中的线程数大于corePoolSize时才会起作用,直到线程池中的线程数不大于corePoolSize)
4)unit:keepAliveTime的单位。
5)workQueue:任务队列,被提交但尚未执行的任务。(相当于候客区)
用于保存任务的阻塞队列。可以使用ArrayBlockingQueue,LinkedBlockingQueue, SynchronousQueue, PriorityBlockingQueue。
6)threadFactory:表示生成线程池中工作线程的线程工厂,用于创建线程,一般默认的的即可。executor 创建新线程的时候会用到。
7)handler:拒绝策略,表示当队列满了并且工作线程大于等于线程池的最大线程数(maximumPoolSize)时如何来拒绝
线程池的拒绝策略。
1) 是什么:等待队列也已经排满了,再也塞不下新任务了,同时,线程池中的max线程也达到了,无法继续为新任务服务。这时候就需要拒绝策略机制合理的处理这个问题。
2) 场景:线程池的任务缓存队列已满并且线程池中的线程数目达到maximumPoolSize时。
3) JDK内置的4种策略:
① ThreadPoolExecutor.AbortPolicy(默认):
丢弃所提交的任务并抛出RejectedExecutionException异常组织系统正常运行。
② ThreadPoolExecutor.DiscardPolicy:
丢弃任务,不做任何处理也不抛出异常。如果允许任务丢失,这是最好的一种方案。
③ ThreadPoolExecutor.DiscardOldestPolicy:
丢弃队列最前面的(即队列中等待最久的任务)任务,然后把当前被拒绝的任务加入队列重新提交。
④ ThreadPoolExecutor.CallerRunsPolicy:
由调用线程(提交任务的线程)处理该任务。"调用者运行"的一种调节机制,该策略既不会抛弃任务,也不会抛出异常,而是将某些任务回退到调用者,
多线程的实现方法/方式? |2
线程池用过吗?ThreadPoolExecutor谈谈你的理解? (sxt2)
1) 创建线程的方式
① 通过继承Thread类,重写run方法;
② 通过实现runable接口;
class MyThread implements Runnable{
@Override
public void run(){
...
}
}
③ 通过实现Callable接口; - 现在常用
class MyThread implements Callable<Integer>{
@Override
public Interger call() throws Exception{
sout("Callable 实现。。。");
return null; // 如,return 1024;
}
}
Runnable、Callable区别
public class CallableDemo{
psvm{
//FutureTask(Callable<V> callbel)
FutureTask<Interger> futureTask = new FutureTask<>(new Mythread());
Thread t1 = new Thread(futureTask,“线程名称”);
t1.start();
sout(futureTask.get()); // 获得 1024返回值
}
}
分支合并(forkjoin
public class CallableDemo{
psvm{ //两个线程,一个main主线程,一个是AAfutureTask
//FutureTask(Callable<V> callbel)
FutureTask<Interger> futureTask = new FutureTask<>(new Mythread());
Thread t1 = new Thread(futureTask,“线程名称”);
t1.start(); // 可合并为:new Thread(futureTask,“AA”).start();
// new Thread(futureTask,“AA”).start(); //共用一个futureTask只计算一次,可以再new
int result01 = 100;
//while(!futureTask.isDone()){ //如果没计算完,折中
//}
int result02 = futureTask.get(); // get()方法建议放在最后
// 要求获得Callable线程的计算记过,如果没有计算完成就要去强求,会导致堵塞,直到计算完成。
sout(result01 + result02); // 1124
}
}
④ 线程池
ThreadPoolExecutor创建线程池
/** 一个简单的Runnable类,需要大约5秒钟来执行其任务。*/
public class MyRunnable implements Runnable {
private String command;
public MyRunnable(String s) {
this.command = s;
}
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " Start. Time = " + new Date());
processCommand();
System.out.println(Thread.currentThread().getName() + " End. Time = " + new Date());
}
private void processCommand() {
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
@Override
public String toString() {
return this.command;
}
}
② 测试程序,ThreadPoolExecutor 构造函数自定义参数创建线程池。
public class ThreadPoolExecutorDemo {
private static final int CORE_POOL_SIZE = 5;
private static final int MAX_POOL_SIZE = 10;
private static final int QUEUE_CAPACITY = 100;
private static final Long KEEP_ALIVE_TIME = 1L;
public static void main(String[] args) {
//通过ThreadPoolExecutor构造函数自定义参数创建
ThreadPoolExecutor executor = new ThreadPoolExecutor(
CORE_POOL_SIZE,
MAX_POOL_SIZE,
KEEP_ALIVE_TIME,
TimeUnit.SECONDS,
new ArrayBlockingQueue<>(QUEUE_CAPACITY),
new ThreadPoolExecutor.CallerRunsPolicy());
// 创建10个
for (int i = 0; i < 10; i++) {
//创建WorkerThread对象(WorkerThread类实现了Runnable 接口)
Runnable worker = new MyRunnable("" + i);
//执行Runnable
executor.execute(worker);
}
//终止线程池
executor.shutdown();
while (!executor.isTerminated()) {
}
System.out.println("Finished all threads");
}
}
/** ① MyCallable.java */
public class MyCallable implements Callable<String> {
@Override
public String call() throws Exception {
Thread.sleep(1000);
//返回执行当前 Callable 的线程名字
return Thread.currentThread().getName();
}
}
/** ② CallableDemo.java */
public class CallableDemo {
public static void main(String[] args) {
//通过ThreadPoolExecutor构造函数自定义参数创建
ThreadPoolExecutor executor = new ThreadPoolExecutor(
5,10,1L,
TimeUnit.SECONDS,
new ArrayBlockingQueue<>(100),
new ThreadPoolExecutor.CallerRunsPolicy());
List<Future<String>> futureList = new ArrayList<>();
Callable<String> callable = new MyCallable();
for (int i = 0; i < 10; i++) {
//提交任务到线程池
Future<String> future = executor.submit(callable);
//将返回值 future 添加到 list,我们可以通过 future 获得执行 Callable 得到的返回值
futureList.add(future);
}
for (Future<String> fut : futureList) {
try {
System.out.println(new Date() + "::" + fut.get());
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
}
//关闭线程池
executor.shutdown();
}
}
区别
public Future<?> submit(Runnable task) {
if (task == null) throw new NullPointerException();
RunnableFuture<Void> ftask = newTaskFor(task, null);
execute(ftask);
return ftask;
}
// 上面方法调用的 newTaskFor 方法返回了一个 FutureTask 对象。
protected <T> RunnableFuture<T> newTaskFor(Runnable runnable, T value) {
return new FutureTask<T>(runnable, value);
}
-- execute()方法:
public void execute(Runnable command) {
...
}
多线程相关:如何停止线程?
|源码|
完整的线程池执行的流程/任务提交流程?
当一个并发任务提交给线程池,线程池分配线程去执行任务的过程:
1) 先判断线程池中核心线程池所有的线程是否都在执行任务。如果不是,则新创建一个线程执行刚提交的任务,否则,核心线程池中所有的线程都在执行任务,则进入第 2 步;
2) 判断当前阻塞队列是否已满,如果未满,则将提交的任务放置在阻塞队列中;否则,则进入第 3 步;
3) 判断线程池中所有的线程是否都在执行任务,如果没有,则创建一个新的线程来执行任务,否则,则交给饱和策略进行处理
https://juejin.im/post/5aeec0106fb9a07ab379574f
说说线程池的底层工作原理?(sxt2)
execute方法源码
RunnableDemo中使用 executor.execute(worker)来提交一个任务到线程池中
// 存放线程池的运行状态 (runState) 和线程池内有效线程的数量 (workerCount)
private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
private static int workerCountOf(int c) {
return c & CAPACITY;
}
//任务队列
private final BlockingQueue<Runnable> workQueue;
public void execute(Runnable command) {
// 如果任务为null,则抛出异常。
if (command == null)
throw new NullPointerException();
// ctl 中保存的线程池当前的一些状态信息
int c = ctl.get();
// 下面会涉及到 3 步 操作
// 1.首先判断当前线程池中之行的任务数量是否小于 corePoolSize
// 如果小于的话,通过addWorker(command, true)新建一个线程,并将任务(command)添加到该线程中;然后,启动该线程从而执行任务。
if (workerCountOf(c) < corePoolSize) {
if (addWorker(command, true))
return;
c = ctl.get();
}
// 2.如果当前之行的任务数量大于等于 corePoolSize 的时候就会走到这里
// 通过 isRunning 方法判断线程池状态,线程池处于 RUNNING 状态才会被并且队列可以加入任务,该任务才会被加入进去
if (isRunning(c) && workQueue.offer(command)) {
int recheck = ctl.get();
// 再次获取线程池状态,如果线程池状态不是 RUNNING 状态就需要从任务队列中移除任务,并尝试判断线程是否全部执行完毕。同时执行拒绝策略。
if (!isRunning(recheck) && remove(command))
reject(command);
// 如果当前线程池为空就新创建一个线程并执行。
else if (workerCountOf(recheck) == 0)
addWorker(null, false);
}
//3. 通过addWorker(command, false)新建一个线程,并将任务(command)添加到该线程中;然后,启动该线程从而执行任务。
//如果addWorker(command, false)执行失败,则通过reject()执行相应的拒绝策略的内容。
else if (!addWorker(command, false))
reject(command);
}
addWorker方法源码 这个方法主要用来创建新的工作线程,如果返回true说明创建和启动工作线程成功,否则的话返回的就是false。
// 全局锁,并发操作必备
private final ReentrantLock mainLock = new ReentrantLock();
// 跟踪线程池的最大大小,只有在持有全局锁mainLock的前提下才能访问此集合
private int largestPoolSize;
// 工作线程集合,存放线程池中所有的(活跃的)工作线程,只有在持有全局锁mainLock的前提下才能访问此集合
private final HashSet<Worker> workers = new HashSet<>();
//获取线程池状态
private static int runStateOf(int c) { return c & ~CAPACITY; }
//判断线程池的状态是否为 Running
private static boolean isRunning(int c) {
return c < SHUTDOWN;
}
/**
* 添加新的工作线程到线程池
* @param firstTask 要执行
* @param core参数为true的话表示使用线程池的基本大小,为false使用线程池最大大小
* @return 添加成功就返回true否则返回false
*/
private boolean addWorker(Runnable firstTask, boolean core) {
retry:
for (;;) {
//这两句用来获取线程池的状态
int c = ctl.get();
int rs = runStateOf(c);
// Check if queue empty only if necessary.
if (rs >= SHUTDOWN &&
! (rs == SHUTDOWN &&
firstTask == null &&
! workQueue.isEmpty()))
return false;
for (;;) {
//获取线程池中线程的数量
int wc = workerCountOf(c);
// core参数为true的话表明队列也满了,线程池大小变为 maximumPoolSize
if (wc >= CAPACITY ||
wc >= (core ? corePoolSize : maximumPoolSize))
return false;
//原子操作将workcount的数量加1
if (compareAndIncrementWorkerCount(c))
break retry;
// 如果线程的状态改变了就再次执行上述操作
c = ctl.get();
if (runStateOf(c) != rs)
continue retry;
// else CAS failed due to workerCount change; retry inner loop
}
}
// 标记工作线程是否启动成功
boolean workerStarted = false;
// 标记工作线程是否创建成功
boolean workerAdded = false;
Worker w = null;
try {
w = new Worker(firstTask);
final Thread t = w.thread;
if (t != null) {
// 加锁
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
//获取线程池状态
int rs = runStateOf(ctl.get());
//rs < SHUTDOWN 如果线程池状态依然为RUNNING,并且线程的状态是存活的话,就会将工作线程添加到工作线程集合中
//(rs=SHUTDOWN && firstTask == null)如果线程池状态小于STOP,也就是RUNNING或者SHUTDOWN状态下,同时传入的任务实例firstTask为null,则需要添加到工作线程集合和启动新的Worker
// firstTask == null证明只新建线程而不执行任务
if (rs < SHUTDOWN ||
(rs == SHUTDOWN && firstTask == null)) {
if (t.isAlive()) // precheck that t is startable
throw new IllegalThreadStateException();
workers.add(w);
//更新当前工作线程的最大容量
int s = workers.size();
if (s > largestPoolSize)
largestPoolSize = s;
// 工作线程是否启动成功
workerAdded = true;
}
} finally {
// 释放锁
mainLock.unlock();
}
//// 如果成功添加工作线程,则调用Worker内部的线程实例t的Thread#start()方法启动真实的线程实例
if (workerAdded) {
t.start();
/// 标记线程启动成功
workerStarted = true;
}
}
} finally {
// 线程启动失败,需要从工作线程中移除对应的Worker
if (! workerStarted)
addWorkerFailed(w);
}
return workerStarted;
}
线程池怎么保证线程一直运行的?
Runnable r = timed ?
workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
workQueue.take();
if (r != null)
return r;
--从阻塞任务队列中取任务,如果设置了allowCoreThreadTimeOut(true) 或者当前运行的任务数大于设置的核心线程数,那么timed =true 。此时将使用workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS)从任务队列中取任务,而如果没有设置,那么使用workQueue.take();取任务,对于阻塞队列,poll(long timeout, TimeUnit unit) 将会在规定的时间内去任务,如果没取到就返回null。take()会一直阻塞,等待任务的添加。线程池能够一直等待任务的执行而不被销毁了,其实也就是进入了阻塞状态而已。
Execuors类实现的几种线程池类型,最后如何返回?
① newFixedThreadPool创建一个固定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。
② newCachedThreadPool创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。
③ newSingleThreadExecutor创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。
④ newScheduledThreadPool 创建一个固定长度线程池,支持定时及周期性任务执行。
线程池如何使用? (sxt2)
1) 架构说明
Java中的线程池是通过Executor框架实现的,该框架中用到了Executor,Executors(辅助工具类,如Arrays),ExecutorService,ThreadPoolExecutor(线程池的底层)这几个类。
2) 编码实现(共5种线程池)-第4中获得/使用Java多线程的方式,线程池
① 了解
-- Executors.newScheduledThreadPool()
池中任务每2'执行一次
-- Java8新出 Executors.newWorkStealingPool(int)
使用目前机器上可用的处理器作为它的并行级别(用的少,面试不怎么考)
② 重点
public interface List<E> extends Collection<E> {
public interface ExecutorService extends Executor {
// 使用
public class MyThreadPoolDemo{
public static void main(String[] args) {
ExecutorService threadPool = Executors.newFixedThreadPool(5);//一池5个处理线程
// ExecutorService threadPool = Executors.newSingleThreadExecutor();//一池1个处理线程
// ExecutorService threadPool = Executors.newCachedThreadPool();//一池N个处理线程
// 模拟10个用户来办理业务,每个用户就是一个来自外部的请求线程
try {
for (int i = 0; i <10 ; i++) { //10个请求
threadPool.execute(()->{ //Lambda
System.out.println(Thread.currentThread().getName()+"\t 办理业务");
});
// TimeUnit.SECONDS.sleep(1);
}
}catch (Exception e){
e.printStackTrace();
}finally {
threadPool.shutdown();//释放
}
}
}
① Executors.newFixedThreadPool(int)
1)创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。
2)newFixedThreadPool创建线程池CorePoolSize和maximumPoolSize值是相等的,使用的LinkedBlockingQueue。
适用:执行长期的任务,性能好很多。
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
② Executors.newSingleThreadExecutor()
1> 创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定的顺序执行。
2> newSingleThreadExecutor将CorePoolSize和maximumPoolSize都设置为1,它使用的LinkedBlockingQueue。
适用:一个任务一个任务执行的场景。
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
单线程线程池newSingleThreadExecutor的应用场景
适用:一个任务一个任务执行的场景
③ Executors.newCachedThreadPool()
1、创建一个可缓存线程池,如果线程池长度超过处理需求,可灵活回收线程池,若无可回收,则新建线程池。
2、将CorePoolSize设置为0,将maximumPoolSize设置为Interger.MAX_VALUE,即无界的,使用的SynchronousQueue,也就是说来了任务就创建线程运行,当线程空闲超过60秒,就销毁线程。
如果主线程提交任务的速度高于maximumPool中线程处理任务的速度时,CachedThreadPool 会不断创建新的线程。极端情况下,这样会导致耗尽 cpu 和内存资源。
适用:执行很多短期异步的小程序或者负载较轻的服务器。
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
1> 首先执行 SynchronousQueue.offer(Runnable task) 提交任务到任务队列。如果当前 maximumPool中有闲线程正在执行SynchronousQueue.poll(keepAliveTime,TimeUnit.NANOSECONDS),那么主线程执行 offer 操作与空闲线程执行的 poll 操作配对成功,主线程把任务交给空闲线程执行,execute()方法执行完成,否则执行下面的步骤 2;
2> 当初始 maximumPool 为空,或者 maximumPool 中没有空闲线程时,将没有线程执行 SynchronousQueue.poll(keepAliveTime,TimeUnit.NANOSECONDS)。这种情况下,步骤 1 将失败,此时 CachedThreadPool 会创建新线程执行任务,execute 方法执行完成;
线程池 newCachedThreadPool线程池的缺点?配置参数?
如上
④ Executors.ScheduledThreadPool()
线程池的好处
加快响应速度
合理利用CPU和内存
统一管理
线程池适应应用的场合
-- 服务器接受到大量请求时,使用线程池技术时非常合适的,它可以大大减少线程的创建和销毁次数,提高服务器的工作效率
--实际上,在开发中,如果需要创建5个以上的线程,那么就可以使用线程池来管理。
3种常见的队列类型
1)直接交换:SynchronousQueue
2)无界队列:LinkedBlockingQueue
3)有界队列:ArrayBlockingQueue
线程池里的线程数量设定为多少比较合适?
-- CPU密集型(加密、计算hash等):最佳线程数为CPU核心数的1-2倍左右。
-- 耗时IO型(读写数据库、文件、网络读写等):最佳线程数一般会大于cpu核心数很多倍,以JVM线程监控显示繁忙情况为依据,保证线程空闲可以衔接上,参考Brain Goetz推荐的计算方法:线程数=CPU核心数*(1+平均等待时间/平均工作时间)
更详细可以进行压测
|设计线程池|
4. java线程你是怎么使用的?
4. 线程池用的多吗?让你设计一个线程池如何设计
5. 如何构造线程池,它的参数,饱和策略?
你再工作中单一的/固定的/可变的三种创建线程池的方法,你用的哪个多?超级大坑(sxt2)
1) 正确答案:一个都不用,我们生产上只使用自定义的。
2) Executors中JDK已经给你提供了,为什么不用?
工作中如何使用线程池的,是否子定义过线程池的使用?(sxt2)
public static void main(String[] args) {
ExecutorService threadPool = new ThreadPoolExecutor(
2, //corePoolSize
5,//maximumPoolSize
1L,//keepAliveTime
TimeUnit.SECONDS,
new LinkedBlockingDeque<Runnable>(3),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.AbortPolicy());
// 银行开启最大8个窗口
try {
for (int i = 0; i <10 ; i++) { //10个请求
threadPool.execute(()->{ //Lambda
System.out.println(Thread.currentThread().getName()+"\t 办理业务");
});
}
}catch (Exception e){
e.printStackTrace();
}finally {
threadPool.shutdown();
}
}
合理配置线程池你是如何考虑的(sxt2)
1) 决定核心线程数两个方面:CUP密集型、IO密集型
//第一步:先获取运行服务器是几核的
System.out.println(Runtime.getRuntime().availableProcessors());
2) CPU密集
3) IO密集型(2种,第1种常被讲,实际应用看效果)
corepoolsize和CPU有什么关系,为什么书上推荐是N+1,线程池适合计算密集型还是IO密集型
public class ThreadPoolUtil {
private static int corePoolSize = 4;
private static int maximumPoolSize = 32;
private static long keepAliveTime = 60;
private static TimeUnit unit = TimeUnit.SECONDS;
private static int maximumTask = 5000;
private static AtomicInteger tid = new AtomicInteger();
private static final ThreadPoolExecutor executor = new ThreadPoolExecutor(corePoolSize, maximumPoolSize, keepAliveTime,
unit, new ArrayBlockingQueue<>(maximumTask), r -> new Thread(r,"skynet-pool-" + tid.incrementAndGet()), new ThreadPoolExecutor.AbortPolicy());
public static Future<?> submit(Runnable r) {
return executor.submit(r);
}
public static <T> Future<T> submit(Callable<T> task) {
return executor.submit(task);
}
public static void main(String[] args) throws InterruptedException {
Runnable runnable = new Runnable() {
@Override
public void run() {
System.out.println(1000);
}
};
submit(runnable);
TimeUnit.SECONDS.sleep(3);
}
}
代码段2:
//不阻塞返回回调结果
ThreadPoolUtil.submit(() -> {
//2. 解析data
// 找到该任务id 更新查询状态
//3.推送消息
// 创建任务相关消息
// 保存到消息表
// 监听接口:通过套接字、监听消息异步通知(发布到redis)
String topic = RedisWsMessageListener.TOPIC_SKYNET_WS_MSG;
String msg = JSON.toJSONString(message);
log.info("推送的消息msg:{},写入topic:{},", msg, topic);
//发布到redis
redisTemplate.convertAndSend(topic, msg);
}
});
笔记_S
21.运行时数据区
私有:程序计数器 Java虚拟机栈 本地方法栈
共享:堆 方法区
** 类加载
作用:
-加载类信息 放在 方法区
加载->连接(验证->准备->解析)->初始化
-加载:
类的全限定名->二进制字节流
字节流的静态存储结构->方法区的运行时数据结构
生成.Class对象->各种数据访问入口
-连接:
-验证:
符合要求,文件格式、元数据、字节码、符号引用
-准备:
类变量-方法区(除实例变量-堆)
分配内存与初始值
-解析
符号引用->直接引用
针对:类/接口、字段、类方法、接口方法、方法类型等(在常量池中)
-初始化
执行类构造器方法()过程
** 类加载器
模型:
启动类加载器/引导类
扩展类
应用程序/系统类
自定义
向父类委托
启动类加载
父类无法完成,子类加载
** 运行时数据区
方法区:
类信息(类加载的)
运行时常量池信息
字符串字面量和数字常量(class文件中常量池部分的内存映射)
(? 即时编译器编译后的代码缓存等)
程序计数器
程序控制流指示器,是个计数器
作用:存储下一条指令的地址
每个线程都有自己的
给字节码解释器提供下一条执行指令
唯一无oom
存储反编译后的指令地址(理解:类似于行号),对应操作指令
Java栈:
线程创建->栈创建,内部保存栈帧
一个方法对应一个栈帧的入栈和出栈
包含方法的局部变量(8基本数据类型,对象的引用地址)、部分结果
-Xss
-Xss1024k 最大256kb
方法两种返回/栈帧弹出 return和异常
栈帧->基本单位存储,存储方法执行的各种数据
-结构:
-局部变量表:
存方法参数和方法体中的局部变量
含基本数据类型、对象引用、返回值类型
方法执行时,jvm使用局部变量表->参数值到参数变量列表的传递
存储单位slot(变量槽)
32位以内的类型只占用一个slot(包括 引用类型、returnAddress类型),64位的类型(long和double)占用两个slot
构造器、实例方法中,对象引用this 都会存放在索引为0的位置
slot重复使用
-操作栈:
变量临时存储空间
后进先出,由字节码指令(pc计数器),进出数据
-动态连接(或指向运行时常量池的方法引用)
栈帧->包含指向运行时常量池中该栈帧所属方法的引用
Java源文件->编译->字节码文件,变量和方法引用->作为符号引用->存在class文件的常量池里
符号引用=>直接引用:通过静态链接|动态链接
-方法返回地址:
存 调用该方法的pc寄存器的值
-一些附加信息
如,对程序调试提供支持的信息
本地方法栈:
native
作用:管理本地方法的调用
堆:
还可以划分线程私有的缓冲区(TLAB)
存储:对象和数组
几乎所有对象实例?有一些对象在栈上分配(逃逸分析、标量替换)
-Xms10m -Xmx10m 堆内存
堆:新生区、老年区、永久区(1.8元空间)
新生-Eden、survivor
-Xms 初始 = -XX:InitialHeapSize
-Xmx 最大 = -XX:MaxHeapSize
老年/新生 占比 -XX:NewRatio = 4
Eden/survivor -XX:SurvivorRatio
Eden 满了MinorGC ,survivor大对象进入老年代
年龄:-XX:MaxTenuringThreshold=N
养老区内存不足 MajorGC 依旧 OOM
部分收集Partial GC
-新生代 MinorGC/YoungGC
-- 会发生STW
-老年代 MajorGC/OldGC -- CMS
--STW更久,最少伴随一次Minor
-混合收集 MixedGC
-- 整个新生代和部分老年代--只有G1
整堆收集FullGC 整Java堆和方法区
--触发:① System.gc() ② 老年代空间不足③ 方法区空间不足④Minor后进入老年代的avg大于年老代可用内存⑤to过小,对象进入老年代,但老年代空间不足
优化:
避免FullGC,缩短STW
分->优化GC性能
** 内存分配策略/对象提升(promotion)规则
-不同年龄对象分配原则
-优先Eden
-大对象 老年代
-长期存活 老年代
-survivor同年纪和大于survivor一半
-空间分配担保 Minor后,survivor无法容纳,进入老年代 -XX:HandlePromotionFailure 是否允许担保
** TLAB
-堆中,每个线程独占,在Eden,线程安全,提升内存分配吞吐量
-XX:TLABWasteTargetPercent 占Eden的百分比
** 逃逸分析技术
-对象逃逸方法失败,栈上分配 无需回收
-标量-无法分解的更小数据,如Java中的原始类型
-逃逸分析,对象不会被外界分配,JIT优化,将对象拆解成若干变量过程,标量替换-好处,不需要分配内存了,减少堆内存占用
-默认打开 -XX:+ElimilnateAllocations
-Hotspot 标量替换 实现 逃逸分析
** 方法区
主要是Class
Person person = new Person();
方法区 Java栈 Java堆
.class
Person 类的 .class 信息存放在方法区中
person 变量存放在 Java 栈的局部变量表中
真正的 person 对象存放在 Java 堆中
堆的逻辑部分,独立于堆的内存空间
类只加载一次
- OOM:定义太多类,方法区溢出
eg:加载大量三方jar包|tomcat部署工程较多(30-50)|大量动态的生成反射类
- 演进:
永久代:更易导致Java程序oom(超过-XX:MaxPermsize上限)
元空间永久代区别:元空间不在虚拟机设置的内存中,使用本地内存
-大小
JDK7永久代:
-XX:Permsize 初始分配空间 mr:20.75M
-XX:MaxPermsize 最大可分配空间 32位机器64M,64位-82M
JDK8元空间:
-XX:MetaspaceSize mr:win 21M
超过 FullGC触发并卸载没用的类(这些类对应类加载器不再存活) 值重置 新界限
-XX:MaxMetaspaceSize mr:-1 无限制
查看:
jinfo -flag MetaspaceSize PID
jinfo -flag MaxMetaspaceSize PID
弊端:mr虚拟机会耗尽所有可用系统内存
- 解决oom
-通过内存映像分析工具对dump的堆转储快照进行分析
-内存中的对象是否必要?即区分内存泄漏还是内存溢出
内存泄漏:大量引用指向某些不会使用的对象,这些对象还和GCROOT关联不会被回收
- 工具查看泄漏对象到GCROOT的引用链。查看为什么不会回收,类信息,找到泄漏代码位置
内存溢出:内存中的对象还都必须存活
-检查虚拟机堆参数(-Xmx与-Xms),调整大小,检查某些对象生命周期过长?持有状态时间过长?减少程序运行期内的内存消耗
方法区内部结构:
-存储内容:
已被jvm加载的类型信息
常量
静态变量
即时编译器编译后的代码缓存等
-结构
类型信息、运行时常量池、静态变量、JIT代码缓存、域信息、方法信息
-- 类型信息:
-- 域信息
-- 方法信息
-- 运行时常量池
https://www.cnblogs.com/tiancai/p/9321338.html
方法区-运行时常量池
Class字节码文件-常量池
- 常量池:
字节码文件包含:类的版本、字段、方法、接口等描述符信息、
及常量池(各种字面量和对类型、域、方法的符号引用)
字面量:文本字符串
被声明为final的常量值
基本数据类型的值
其他
符号引用:类和结构的完全限定名
字段名称和描述符
方法名称和描述符
- 为什么用它
不使用常量池,类信息、方法信息等要记录在当前字节码文件,文件过大,需要的结构信息记录在常量池,通过引用的方式加载、调用所需结构
- 有什么?
数量值、字符串值、类引用、字段引用、方法引用
- 常量池,可以看做是一张表,虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量等信息
- 运行时常量池:
方法区的一部分
常量池表-Class字节码文件的一部分,存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后放在方法区的运行时常量池中
演进:
1.7 8 6 运行时数据区的变动
1.6 永久区->方法区 静态变量
1.7 去永久区->方法区 字符串常量池、静态变量 放进了堆
1.8 元空间 ->方法区 存放类信息
字符串常量池、静态变量 还在堆
- jdk6
方法区(永久代):
类型信息、域信息、方法信息
JIT代码缓存、静态变量
运行时常量池[字符串常量池StringTable]
- jdk7:
类型信息、域信息、方法信息
JIT代码缓存
运行时常量池
堆:静态变量、StringTable
- jdk8:
无永久代,类型信息,字段,方法,常量保存在本地内存的元空间,但字符串常量池、静态变量仍然在堆中。
为什么使用元空间?
- 虚拟机融合
- 为永久代设置空间大小很难确定
动态加载类过多,OOM
- 对永久代调优困难
方法区的回收:常量池废弃的常量、不再用的类型
调优 为降低FullGC
方法区回收效果难以满意,尤其是类型的卸载、条件苛刻
为什么移动字符串常量池?
- 永久代回收效率低,FullGC触发 老年代空间不足、永久代不足
- 开发中大量字符串被创建,回收效率低,会导致永久代内存不足,放在堆里,能及时回收内存
静态变量放在哪?
-6 7 永久代 8 堆
- 静态变量对应的对象实体使用存在堆空间(只要是对象实例必然会在Java堆中分配)
方法区类回收?
总结?
MinorGC 新生区
MajorGC 老年区
FullGC 整个堆和方法区
** 对象
创建对象?
- new
- clone()
-
步骤?
1判断对象对应的类是否加载、连接、初始化
-new指令
-检查指令参数能否在元空间的常量池中定位到一个类的符号引用,
-检查这个符号引用代表的类是否被加载、解析、初始化 即类元数据是否存在
-未加载,双亲委派模式下,类加载器以ClassLoader+包名+类名为key查找.class文件,未找到,异常,找到,类加载
2为对象分配内存
-内存规整 -> 指针碰撞
-不规整 -> 空闲列表分配
指针碰撞:
用过的一边,空闲的一边,中间指针为分界点指示器,挪动对象大小
空闲列表:
jvm维护列表,记录哪些可用,给对象分配足够空间,更新表
-堆规整?->由采用的垃圾收集器是否带有压缩功能决定,如标记清除 会有很多内存碎片
3处理并发安全问题
-cas+重试失败、区域加锁保证更新的原子性
-每个线程预先分配TLAB -XX:+/-UseTLAB参数设置(区域加锁机制)
-Eden区给给个线程分配一块区域
4初始化分配到的空间
所有属性设置默认值,保证对象实例字段在不赋值时可以直接使用
5设置对象的对象头
-对象所属类(即类的元数据信息)、对象hashCode、对象GC信息、锁信息等数据存储在对象的对象头
6执行init方法进行初始化
对象的内存布局?
1对象头
-运行时元数据(MarkWord)
哈希值(hashcode)
GC分代年龄
锁状态标志
线程持有锁
偏向线程ID
偏向时间戳
-类型指针
指向方法区中存放的类元信息 确定该对象所属类型
数组长度:对象是数组,还需记录数组长度
2实例数据
是对象真正存储的有效信息
3对齐填充
非必须 占位符作用
对象访问?
如何通过栈帧中的对象引用访问到其内部的对象实例?
-定位,通过栈上的reference访问
-句柄访问
优点:reference中存储稳定句柄地址,对象被移动(垃圾收集时移动对象很普遍)时只会改变句柄中实例数据指针即可,reference本身不需要被修改
缺点:在堆空间中开辟了一块空间作为句柄池,句柄池本身也会占用空间;通过两次指针访问才能访问到堆中的对象,效率低
-直接访问(Hotspot采用)
优点:直接指针是局部变量表中的引用,直接指向堆中的实例,在对象实例中有类型指针,指向的是方法区中的对象类型数据
缺点:对象被移动(垃圾收集时移动对象很普遍)时需要修改 reference 的值
** 垃圾回收器
HotSpot回收器?(连线可搭配)
Serial ParNew Parallel Scaveage
G1
CMS Serial Old(MSC) Parallel Old
-jdk8:mr:Parallel Scavenge、Parallel Old
Serial:
-分为Serial、SerialOld
-单线程 垃圾回收线程开始时,业务线程必须暂停
Serial-复制、SerialOld-标记压缩
ParNew:
-多线程
-多条垃圾回收线程并行工作,业务线程处于等待状态
-复制算法
ParallelScnvenge:
-多线程并行
-多条垃圾回收线程并行工作,业务线程处于等待状态
-复制算法
ParallelOld:
-ParallelScnvenge的老年代版本
-多线程 等待
-标记压缩
CMS:
-以获取最短回收停顿时间为目标的收集器
-多线程
垃圾线程和业务线程可以一起执行
-标记清除
-步骤:
初始标记-GCRoot能直接关联到的对象
并发标记-和业务并发
重新标记-修正并发标记期间的变动部分-不能和业务并发
并发清除
-并发标记问题:
1漏标-非垃圾对象后面引用消失,浮动垃圾 重新标记
2错标-垃圾对象后面又被引用
-解决:三色标记算法
漏标:CMS重新标记 A(黑)变成灰色
-CMS大bug
没有jdk版本默认CMS
并发标记漏标:remark阶段,必须从头扫描一遍
G1:
-面向服务端
-步骤:
初始标记
并发标记
最终标记
筛选回收
-优点:并行与并发、分代收集、空间整合、可预测停顿
2 安全点
--1)概念:
当用户线程停顿下来之后,其实并不需要一个不漏地检查完所有执行上下文和全局的引用位置,虚拟机会在特定的位置记录下栈和寄存器里哪些位置是引用。这些特定位置被称为安全点(Safepoint)。
--2)选定:
安全点位置的选取基本上是以“是否具有让程序长时间执行的特征”为标准进行选定的,因为每条指令执行的时间都非常短暂,程序不太可能因为指令流长度太长这样的原因而长时间执行,“长时间执行”的最明显特征就是指令序列的复用,例如方法调用、循环跳转、异常跳转等都属于指令序列复用,所以只有具有这些功能的指令才会产生安全点。
--3)使用:
安全点的设定,也就决定了用户程序执行时并非在代码指令流的任意位置都能够停顿下来开始垃圾收集,而是强制要求必须执行到达安全点后才能够暂停。
--4)如何保证到达
如何在垃圾收集发生时让所有线程(这里其实不包括执行JNI调用的线程)都跑到最近的安全点,然后停顿下来?两种方案可供选择:
① 抢先式中断(PreemptiveSuspension)
抢先式中断不需要线程的执行代码主动去配合,在垃圾收集发生时,系统首先把所有用户线程全部中断,如果发现有用户线程中断的地方不在安全点上,就恢复这条线程执行,让它一会再重新中断,直到跑到安全点上。现在几乎没有虚拟机实现采用抢先式中断来暂停线程响应GC事件。
②主动式中断(VoluntarySuspension)
思想是当垃圾收集需要中断线程的时候,不直接对线程操作,仅仅简单地设置一个标志位,各个线程执行过程时会不停地主动去轮询这个标志,一旦发现中断标志为真时就自己在最近的安全点上主动中断挂起。轮询标志的地方和安全点是重合的,另外还要加上所有创建对象和其他需要在Java堆上分配内存的地方,这是为了检查是否即将要发生垃圾收集,避免没有足够内存分配新对象。
3 安全区域
--安全点的问题:程序不执行
安全点机制保证了程序执行时,在不太长的时间内就会遇到可进入垃圾收集过程的安全点。但是,程序“不执行”的时候呢?
--解决:安全区域
--概念:
安全区域是指能够确保在某一段代码片段之中,引用关系不会发生变化,因此,在这个区域中任意地方开始垃圾收集都是安全的。也可以把安全区域看作被扩展拉伸了的安全点。
--使用:
当用户线程执行到安全区域里面的代码时,首先会标识自己已经进入了安全区域,那样当这段时间里虚拟机要发起垃圾收集时就不必去管这些已声明自己在安全区域内的线程了。当线程要离开安全区域时,它要检查虚拟机是否已经完成了根节点枚举(或者垃圾收集过程中其他需要暂停用户线程的阶段),如果完成了,那线程就当作没事发生过,继续执行;否则它就必须一直等待,直到收到可以离开安全区域的信号为止。
--程序不执行的概念:
程序不执行就是没有分配处理器时间,典型的场景便是用户线程处于Sleep状态或者Blocked状态,这时候线程无法响应虚拟机的中断请求,不能再走到安全的地方去中断挂起自己,虚拟机也显然不可能持续等待线程重新被激活分配处理器时间。
0 项目参数配置
1)icreditb 82
java -XX:+PrintCommandLineFlags:
-XX:InitialHeapSize=62788032
-XX:MaxHeapSize=1004608512
-XX:+PrintCommandLineFlags
-XX:+UseCompressedClassPointers
-XX:+UseCompressedOops -XX:+UseParallelGC
1 通用参数
2)-XX:+PrintGCDetails:获取更详细信息
--显示
新生代GC日志
老年代和永久区的使用情况
会使虚拟机在退出前打印堆的详细信息,详细信息描述了当前堆的各个区间的使用情况
--版本:
-Xlog:gc*:更详细 JDK9、JDK10
2 堆内存配置
-Xms 初始 = -XX:InitialHeapSize
-Xmx 最大 = -XX:MaxHeapSize
--当前总内存在-Xms和-Xmx之间,从-Xms开始根据需求向上增长;当前空闲内存为当前总内存减去当前已使用的空间;
--建议:
实际工作中,可以将初始堆-Xms与最大堆-Xmx设置为相等。好处是,可以减少程序运行时进行垃圾回收的次数,从而提高程序的性能。
3 新生代老年代配置
-Xmn:设置新生代的大小,一般设置为整个堆空间的1/3到1/4。
-XX:NewRatio = 4 老年/新生 占比
-XX:SurvivorRatio=2新生代中eden/from|to区的比例
eden区与from区的比值为2∶1,故eden区为512KB。总可用新生代大小为512KB+256KB=768KB,新生代总大小为512KB+256KB+256KB=1024KB=1MB。
-XX:MaxTenuringThreshold=N 年龄
-XX:HandlePromotionFailure 是否允许担保
-XX:TLABWasteTargetPercent TLAB占Eden的百分比
-默认打开 -XX:+ElimilnateAllocations 默认打开逃逸分析技术,对象逃逸方法失败,栈上分配 无需回收
3 非堆
1)栈内存
-Xss:栈内存大小
--可用的栈内存=进程最大内存-堆内存-方法区内存-程序计数器内存-虚拟机本身耗费内存
--操作系统分配给每个进程的内存是有限制的,譬如32位Windows的单个进程最大内存限制为2GB。HotSpot虚拟机提供了参数可以控制Java堆和方法区这两部分的内存的最大值,那剩余的内存即为2GB(操作系统限制)减去最大堆容量,再减去最大方法区容量,由于程序计数器消耗内存很小,可以忽略掉,如果把直接内存和虚拟机进程本身耗费的内存也去掉的话,剩下的内存就由虚拟机栈和本地方法栈来分配了。因此为每个线程分配到的栈内存越大,可以建立的线程数量自然就越少,建立线程时就越容易把剩下的内存耗尽,内存溢出。
4 程序内存
程序占用
1 Serial收集器
1)单线程
单线程工作的收集器,不仅是使用一个处理器或一条收集线程去完成垃圾收集工作,更强调在进行垃圾收集时,必须暂停其他所有工作线程,“Stop The World”直到它收集结束。
2)优缺点
--优:简单高效
2 ParNew收集器
--ParNew收集器实质上是Serial收集器的多线程并行版本
--ParNew收集器除了支持多线程并行收集之外,其他与Serial收集器相比并没有太多创新之处
--ParNew是多线程并行的,也就是说当多条垃圾回收线程并行工作时,此时的业务线程处于等待状态
--除了Serial收集器外,目前只有它能与CMS收集器配合工作
3 Parallel Scavenge收集器
--Parallel Scavenge 是一个年轻代的垃圾回收器,也就是说Parallel工作在年轻代
--Parallel Scavenge是多线程并行的,也就是说当多条垃圾回收线程并行工作时,此时的业务线程处于等待状态
--采用复制算法实现
--JDK1.8默认采用的垃圾回收器:Parallel Scavenge、Parallel Old
吞吐量
--Parallel Scavenge收集器的特点是它的关注点与其他收集器不同,CMS等收集器的关注点是尽可能
地缩短垃圾收集时用户线程的停顿时间,而Parallel Scavenge收集器的目标则是达到一个可控制的吞吐
量(Throughput)。
如果虚拟机完成某个任务,用户代码加上垃圾收集总共耗费了100分钟,其中垃圾收集花掉1分钟,那吞吐量就是99%。停
--参数控制:
器提供了两个参数用于精确控制吞吐量,分别是控制最大垃圾收集停顿时间的-XX:MaxGCPauseMillis参数以及直接设置吞吐量大小的-XX:GCTimeRatio参数。
----XX:MaxGCPauseMillis参数允许的值是一个大于0的毫秒数,收集器将尽力保证内存回收花费的
时间不超过用户设定值。
----XX:GCTimeRatio参数的值则应当是一个大于0小于100的整数,也就是垃圾收集时间占总时间的
比率,相当于吞吐量的倒数。
自适应调节策略
Parallel Scavenge收集器还有一个参数-XX:+UseAdaptiveSizePolicy值得我们关注。这是一
个开关参数,当这个参数被激活之后,就不需要人工指定新生代的大小(-Xmn)、Eden与Survivor区
的比例(-XX:SurvivorRatio)、晋升老年代对象大小(-XX:PretenureSizeThreshold)等细节参数
了,虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时
间或者最大的吞吐量。这种调节方式称为垃圾收集的自适应的调节策略(GC Ergonomics)
4 Serial Old收集器
Serial Old是Serial收集器的老年代版本,它同样是一个单线程收集器,使用标记-整理算法。这个收
集器的主要意义也是供客户端模式下的HotSpot虚拟机使用。如果在服务端模式下,它也可能有两种用
途:一种是在JDK 5以及之前的版本中与Parallel Scavenge收集器搭配使用[1],另外一种就是作为CMS
收集器发生失败时的后备预案,在并发收集发生Concurrent Mode Failure时使用。
5 Parallel Old收集器
Parallel Old是Parallel Scavenge收集器的老年代版本,支持多线程并发收集,基于标记-整理算法实
现
Parallel Old收集器出现后,“吞吐量优先”收集器终于有了比较名副其实的搭配组合,在注重
吞吐量或者处理器资源较为稀缺的场合,都可以优先考虑Parallel Scavenge加Parallel Old收集器这个组
合。
6 CMS收集器
1. 概念
-- CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。
-- “Mark Sweep”,可以看出CMS收集器是基于标记-清除算法实现的。
-- -XX:+UseConcMarkSweepGC来开启CMS收集器
3)CMS是一款基于“标记-清除”算法实现的收集器,收集结束时会有大量空间碎片产生。
空间碎片过多时,将会给大对象分配带来很大麻烦,往往会出现老年代还有很多剩余空间,但就是无法找 到足够大的连续空间来分配当前对象,而不得不提前触发一次Full GC的情况。为了解决这个问题,CMS收集器提供了一个-XX:+UseCMS-CompactAtFullCollection开关参数(默认是开启的,此参数从 JDK9开始废弃),用于在CMS收集器不得不进行Full GC时开启内存碎片的合并整理过程,由于这个 内存整理必须移动存活对象,(在Shenandoah和ZGC出现前)是无法并发的。这样空间碎片问题是解 决了,但停顿时间又会变长,因此虚拟机设计者们还提供了另外一个参数-XX:CMSFullGCsBefore- Compaction(此参数从JDK9开始废弃),这个参数的作用是要求CMS收集器在执行过若干次(数量 由参数值决定)不整理空间的Full GC之后,下一次进入Full GC前会先进行碎片整理(默认值为0,表 示每次进入Full GC时都进行碎片整理)。
7 G1收集器
1. Garbage First(简称G1)收集器概念
--开创了收集器面向局部收集的设计思路和基于Region(区域)的内存布局形式。
--全功能的垃圾收集器
--JDK 9,G1宣告取代Parallel Scavenge加Parallel Old组合,成为服务端模式下的默认垃圾收集器
--可以由用户指定期望的停顿时间
通常把期望停顿时间设置为一两百毫秒或者两三百毫秒会是比较合理的。
-- -XX:MaxGCPauseMillis参数指定的停顿时间
2. 区域划分
--实现:
G1不再坚持固定大小以及固定数量的分代区域划分,而是把连续的Java堆划分为多个大小相等的独立区域(Region),每一个Region都可以根据需要,扮演新生代的Eden空间、Survivor空间,或者老年代空间。收集器能够对扮演不同角色的Region采用不同的策略去处理。
--新生代和老年代不再是固定,它们都是一系列区域(不需要连续)的动态集合。
--Humongous区域:专门用来存储大对象。
G1认为只要大小超过了一个Region容量一半的对象即可判定为大对象。
每个Region的大小可以通过参数-XX:G1HeapRegionSize设定,取值范围为1MB~32MB,且应为2的N次幂。而对于那些超过了整个Region容量的超级大对象,将会被存放在N个连续的Humongous Region之中,G1的大多数行为都把Humongous Region作为老年代的一部分来进行看待。
3. 如何停顿
--为什么建立可预测的停顿时间模型?
因为它将Region作为单次回收的最小单元,即每次收集到的内存空间都是Region大小的整数倍,这样可以有计划地避免
在整个Java堆中进行全区域的垃圾收集。
--更具体的处理思路:
让G1收集器去跟踪各个Region里面的垃圾堆积的“价值”大小,价值即回收所获得的空间大小以及回收所需时间的经验值,然后在后台维护一个优先级列表,每次根据用户设定允许的收集停顿时间(使用参数-XX:MaxGCPauseMillis指定,默认值是200毫秒),优先处理回收价值收益最大的那些Region。这种使用Region划分内存空间,以及具有优先级的区域回收方式,保证了G1收集器在有限的时间内获取尽可能高的收集效率。
4. 问题解决
1)跨Region引用对象如何解决?
使用记忆集避免全堆作为GC Roots扫描:
但G1上记忆集的应用其实要复杂很多,每个Region都维护有自己的记忆集,这些记忆集会记录下别的Region指向自己的指针,并标记这些指针分别在哪些卡页的范围之内。
G1的记忆集在存储结构的本质上是一种哈希表,Key是别的Region的起始地址,Value是一个集合,里面存储的元素是卡表的索引号。这种“双向”的卡表结构(卡表是“我指向谁”,这种结构还记录了“谁指向我”)比原来的卡表实现起来更
复杂,同时由于Region数量比传统收集器的分代数量明显要多得多,因此G1收集器要比其他的传统垃圾收集器有着更高的内存占用负担。根据经验,G1至少要耗费大约相当于Java堆容量10%至20%的额外内存来维持收集器工作。
2)在并发标记阶段如何保证收集线程与用户线程互不干扰地运行?
①首先要解决的是用户线程改变对象引用关系时,必须保证其不能打破原本的对象图结构,导致标记结果出现错误:
CMS收集器采用增量更新算法实现,而G1收集器则是通过原始快照(SATB)算法来实现的。
②回收过程中新创建对象的内存分配上?
程序要继续运行就肯定会持续有新对象被创建,G1为每一个Region设计了两个名为TAMS(Top at Mark Start)的指针,把Region中的一部分空间划分出来用于并发回收过程中的新对象分配,并发回收时新分配的对象地址都必须要在这两个指针位置以上。G1收集器默认在这个地址以上的对象是被隐式标记过的,即默认它们是存活的,不纳入回收范围。与CMS中的“Concurrent Mode Failure”失败会导致Full GC类似,如果内存回收的速度赶不上内存分配的速度,
G1收集器也要被迫冻结用户线程执行,导致Full GC而产生长时间“Stop The World”。
3)怎样建立起可靠的停顿预测模型?用户通过-XX:MaxGCPauseMillis参数指定的停顿时间只意味着垃圾收集发生之前的期望值,但G1收集器要怎么做才能满足用户的期望呢?
--G1收集器的停顿预测模型是以衰减均值(Decaying Average)为理论基础来实现的,在垃圾收集过程中,G1收集器会记录每个Region的回收耗时、每个Region记忆集里的脏卡数量等各个可测量的步骤花费的成本,并分析得出平均值、标准偏差、置信度等统计信息。这里强调的“衰减平均值”是指它会比普通的平均值更容易受到新数据的影响,平均值代表整体平均状态,但衰减平均值更准确地代表“最近的”平均状态。换句话说,Region的统计状态越新越能决定其回收的价值。然后通过这些信息预测现在开始回收的话,由
哪些Region组成回收集才可以在不超过期望停顿时间的约束下获得最高的收益。
5. 运作过程4个步骤
1)初始标记(Initial Marking):仅仅只是标记一下GC Roots能直接关联到的对象,并且修改TAMS指针的值,让下一阶段用户线程并发运行时,能正确地在可用的Region中分配新对象。这个阶段需要停顿线程,但耗时很短,而且是借用进行Minor GC的时候同步完成的,所以G1收集器在这个阶段实际并没有额外的停顿。
2)并发标记(Concurrent Marking):从GC Root开始对堆中对象进行可达性分析,递归扫描整个堆里的对象图,找出要回收的对象,这阶段耗时较长,但可与用户程序并发执行。当对象图扫描完成以后,还要重新处理SATB记录下的在并发时有引用变动的对象。
3)最终标记(Final Marking):对用户线程做另一个短暂的暂停,用于处理并发阶段结束后仍遗留下来的最后那少量的SATB记录。
4)筛选回收(Live Data Counting and Evacuation):负责更新Region的统计数据,对各个Region的回收价值和成本进行排序,根据用户所期望的停顿时间来制定回收计划,可以自由选择任意多个Region构成回收集,然后把决定回收的那一部分Region的存活对象复制到空的Region中,再清理掉整个旧 Region的全部空间。这里的操作涉及存活对象的移动,是必须暂停用户线程,由多条收集器线程并行完成的。
6. 优缺点
① 无限制。默认的停顿目标为两百毫秒,一般来说,回收阶段占到几十到一百甚至接近两百毫秒都很正常。
如果把停顿时间调得非常低,譬如设置为二十毫秒,很可能出现的结果就是由于停顿目标时间太短,导致每次选出来的回收集只占堆内存很小的一部分,收集器收集的速度逐渐跟不上分配器分配的速度,导致垃圾慢慢堆积。应用运行时间一长,最终占满堆引发Full GC反而降低性能,所以通常把期望停顿时间设置为一两百毫秒或者两三百毫秒会是比较合理的。
--优点:
① 可以指定最大停顿时间、分Region的内存布局、按收益动态确定回收集,
② G1从整体来看是基于“标记-整理”算法实现的收集器,但从局部(两个Region 之间)上看又是基于“标记-复制”算法实现,无论如何,这两种算法都意味着G1运作期间不会产生内存空间碎片,垃圾收集完成之后能提供规整的可用内存。
有利于程序长时间运行,在程序为大对象分配内存时不容易因无法找到连续内存空间而提前触发下一次收集。
--缺点:与cms相比
① 用户程序运行过程
中,G1无论是为了垃圾收集产生的内存占用(Footprint)还是程序运行时的额外执行负载
(Overload)都要比CMS要高。
② 然G1和CMS都使用卡表来处理跨代指针,但G1的卡表实现更为复杂,而且堆中每个Region,无论扮演的是新生代还是老年代角色,都必须有一份卡表,这导致G1的记忆集(和
其他内存消耗)可能会占整个堆容量的20%乃至更多的内存空间;相比起来CMS的卡表就相当简单,只有唯一一份,而且只需要处理老年代到新生代的引用,反过来则不需要,由于新生代的对象具有朝生夕灭的不稳定性,引用变化频繁,能省下这个区域的维护开销是很划算的。
③ 负载不同
譬如它们都使用到写屏障,CMS用写后屏障来更新维护卡表;而G1除了使用写后屏障来进行同样的(由于G1的卡表结构复杂,其实是更烦琐的)卡表维护操作外,为了实现原始快照搜索(SATB)算法,还需要使用写前屏障来跟踪并发时的指针变化情况。相比起增量更新算法,原始快照搜索能够减少并发标记和重新标记阶段的消耗,避免CMS那样在最终标记阶段停顿时间过长的缺点,但是在用户程序运行过程中确实会产生由跟踪引用变化带来的额外负担。由于G1对写屏障的复杂操作要比CMS消耗更多的运算资源,所以CMS的写屏障实现是直接的同步操作,而G1就不得不将其实现
为类似于消息队列的结构,把写前屏障和写后屏障中要做的事情都放到队列里,然后再异步处理。
-- jvm学习:https://www.zybuluo.com/songhanshi/note/1733752
配置过java启动设置吗
没有,我只用过-xms等指令改过JVM参数,和jinfo看参数
-XMX -XSS -XMN
说说对象创建到消亡的过程
https://blog.csdn.net/u012312373/article/details/46718911
https://blog.csdn.net/qq_25005909/article/details/78981512
JMM规定了内存主要划分为主内存和工作内存两种。此处的主内存和工作内存跟JVM内存划分(堆、栈、方法区)是在不同的层次上进行的,如果非要对应起来,主内存对应的是Java堆中的对象实例部分,工作内存对应的是栈中的部分区域,从更底层的来说,主内存对应的是硬件的物理内存,工作内存对应的是寄存器和高速缓存。
1. 线程的工作内存指的是什么,在内存的哪个地方
* JVM将内存组织为主内存和工作内存两个部分。
* 主内存主要包括本地方法区和堆。每个线程都有一个工作内存,主要包括两个部分,一个是属于该线程私有的栈和对主存部分变量拷贝的寄存器(包括程序计数器PC和cup工作的高速缓存区)。
① 所有的变量都存储在主内存中,对于所有线程都是共享的。
② 每条线程都有自己的工作内存,工作内存中保存的是主存中某些变量的拷贝,线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存中的变量。
③ 线程之间无法直接访问对方的工作内存中的变量,线程间变量的传递均需要通过主内存来完成。
待完善:https://aalion.github.io/2019/12/08/concurrency12/
https://www.jianshu.com/p/679ad52eca05
JRE和JDK的区别?
JVM了解么?
主流:HotSpot VM
JVM的内存模型可以说下吗?
(说一下Java虚拟机内存区域划分、各区域的介绍、1.8&1.7版本迭代)
Java虚拟机内存的各个区域?
Java虚拟机在执行Java程序的过程中会把它所管理的内存划分为若干个不同的数据区域。(运行时数据区域,强调对内存空间的划分
Java虚拟机内存的各个区域-分
【1】程序计数器?
常量池、运行时常量池、字符串常量池中都存储的什么
内存模型,堆和栈都有什么?
(问法不够准确,此处只问内存模型,应该是JMM,后面又问到堆栈应该是想问JVM内存,先按照JVM的角度回答,持续关注...)
JVM堆内存划分
(Java垃圾回收:
new一个对象? -jvm3
jvm怎么知道对象属于哪个类?
|OutOfMemoryError异常-内存溢出异常|
堆溢出?
栈溢出? (HotSpot-虚拟机栈和本地方法栈)
调整栈内存jvm参数知道吗?常用的jvm参数有那些?
指令 | 作用 |
---|---|
-Xss | 指定线程的栈大小。栈是每个线程私有的内存空间。 |
递归10w次会出现什么?(OOM)
栈溢出异常,通过什么方式来解决?
怎么让方法区溢出?
jvms:一个系统不断产生新的类,而没有回收,最终可能导致永久区溢出。
// jdk1.6 -XX:MaxPermSize=5m
public class PermOOM{
public static void main(String[] args) {
try {
for (int i = 0; i <100000 ; i++) {
// 每次循环都生成一个新的类(是类,而非对象实例)
CglibBean bean = new CglibBean("geym.jvm.ch3.perm.bean"+i,new HashMap());
}
}catch (Error e){
e.printStackTrace();
}
}
}
// 结果
// Exception in thread "main" java.lang.OutOfMemoryError: PermGen space
解决永久区溢出,从以下几个方面考虑(jvms)
-- 增加MaxPermSize的值
-- 减少系统需要的类的数量
-- 使用ClassLoader合理地装载各个类,并定期进行回收
遇到过的OOM?
OOM 如何排查以及优化/OOM问题怎么定位(线上?) -P50
常规的处理方法(jvm3)
Java会不会内存泄露?怎样会泄露?
Java 内存泄漏问题,解释一下什么情况下会出现?
垃圾回收,堆区为什么那么分
Java垃圾回收简单讲一下,里面的算法?
JVM 垃圾回收的是如何确定垃圾?
Person p = null;
java垃圾回收,如何判断一个对象需要回收
引用计数算法 和 可达性分析算法
缺点
必须要配合大量额外处理才能保证正确地工作,如单纯的引用计数就很难解决对象之间相互循环引用的问题。
Object a = new Object();
Object b = new Object();
a=b;
b=a;
a=b=null; //这样就导致gc无法回收他们。
可达性分析算法
通过一系列称为“GC Roots”的根对象作为起始节点集,从这些节点开始,根据引用关系向下搜索,搜索过程所走过的路径称为“引用链”(Reference Chain),如果某个对象到GC Roots间没有任何引用链相连,或者用图论的话来说就是从GC Roots到这个对象不可达时,则证明此对象是不可能再被使用的。
缺点:实现比较复杂、需要分析大量数据,消耗大量时间、分析过程需要GC停顿(引用关系不能发生变化),即停顿所有Java执行线程(称为"Stop The World",是垃圾回收重点关注的问题)。
是否知道什么是GC Roots?(jvm3)
哪些对象可以作为gcroot(jvm2)
public class GCRootDemo{
private static GCRootDemo2 t2;//第2种,static静态,一份全部实例变量共用?被加载进方法区,
// Java7方法区为永久代;GCRootDemo2其他对象
private static final GCRootDemo3 t3 = new GCRootDemo3(8);//static final常量引用
public static void m1(){
GCRootDemo t1 = new GCRootDemo();//第1种:m1方法在栈中,t1为方法中的局部变量
System.gc();
System.out.println("第一次GC完成");
}
public static void main(String[] args) {
m1();
}
}
引用?
判断一个对象生存还是死亡?
4.jvm卡表(Card Table)?
安全区域(Safe Region),记忆集
垃圾回收算法有哪些
垃圾回收算法,为什么老年代和新生代不同
垃圾收集算法新生代和老年代分别用什么算法
如果对象大部分都是存活的,少部分需要清除,用什么算法
名词概念
说说GC的流程
什么时候对象会到老年代,老年代的更新机制
垃圾回收器了解吗?
为何需要垃圾回收?
有哪些gc收集器?
垃圾回收器在哪块?
垃圾回收器(CMS)详细过程。哪个阶段出现STW?
垃圾收集器CMS出现问题了怎么办?
java内存管理?
什么时候对象会到老年代,老年代的更新机制?
操作系统层面是怎么分配内存的 91
https://blog.csdn.net/qq_32635069/article/details/74838187
类加载的顺序?
有哪些操作会触发类加载?
类加载过程?
详细说说类加载的过程,静态代码块执行在哪个阶段?
静态代码块在初始化阶段执行
https://blog.csdn.net/qq_36839438/article/details/106738514
https://blog.csdn.net/qq_38159458/article/details/105865964
类加载器的4个种类
1)启动类加载器:这个类加载器负责放在目录中的,或者被-Xbootclasspath参数所指定的路径中的,并且是虚拟机识别的类库。用户无法直接使用。
2)扩展类加载器:这个类加载器由AppClassLoader\lib\ext由sun.misc.Launcher实现。是ClassLoader中getSystemClassLoader()方法的返回值。它负责用户路径(ClassPath)所指定的类库。用户可以直接使用。如果用户没有自己定义类加载器,默认使用这个。
4)自定义加载器:用户自己定义的类加载器。
双亲委派模型
双亲委派模型:
为啥要双亲加载
双亲委派机制,怎么打破
tomcat
JVM堆上会不会产生线程安全问题 pP48
那比如你在项目里写了一个Class A,然后在某一个jar包里也有一个Class A,比如com.a.A,那么这两个class你觉得哪个先被加载,会出现什么问题(不会,求了答案,告诉我说他也不清楚,就是考考我对这块有没有自己的理解😑)
字节码是什么?
字节码:Java程序无须重新编译便可在多种不同的计算机上运行。
字节码(Byte-code)是一种包含执行程序,由一序列 op 代码/数据对组成的二进制文件,是一种中间码。 javap -c
1 Mysql逻辑架构:
--MySQL 5.5.5 版本开始成为了默认存储引擎
--不同的存储引擎共用一个Server 层
查询的执行流程
select * from user where userId=1;
1 Mysql逻辑架构
1)客户端:
作用:与server层建立连接,发送查询请求、接受响应的结果集。
2)Server层
-包含连接器、查询缓存、分析器、优化器、执行器等组件,完成mysql大部分功能;
-功能:查询解析、分析、优化、缓存以及所有的内置函数(例如,日期、时间、数学和加密函数)-以及存储过程、触发器、视图等跨存储引擎的功能;
-通用的日志模块binlog
3)存储引擎层
-作用:负责MySQL中数据的存储和提取。
-支持多个存储引擎,例如:InnoDB、MyISAM等;
-InnoDB:MySQL 5.5.5版本开始成为默认存储引擎,InnoDB引擎包含自带的日志模块redolog
2 查询流程梳理
3 组件
1 日志
redo log 是InnoDB引擎特有的日志,binlog是Server层自己的日志;
1)redolog
--作用:确保事务的持久性。
以防在发生故障的时间点,还有脏页未写入磁盘,在重启mysql的时候,根据redo log进行重做,从而达到事务的持久性这一特性。
2)binlog
https://www.sohu.com/a/275633000_684445
https://www.jianshu.com/p/c16686b35807
一个定义,两个误解,三个用途,四个常识
1>定义
--binlog是记录所有数据库表结构变更(例如CREATE、ALTER TABLE…)以及表数据修改(INSERT、UPDATE、DELETE…)的二进制日志。
--binlog不会记录SELECT和SHOW这类操作,因为这类操作对数据本身并没有修改,但你可以通过查询通用日志来查看MySQL执行过的所有语句。
--注:update操作没有造成数据变化,也是会记入binlog
2>结构
binlog称之为二进制日志,这个二进制日志包括两类文件:
索引文件(文件名后缀为.index)用于记录哪些日志文件正在被使用
日志文件(文件名后缀为.00000*)记录数据库所有的DDL和DML(除了数据查询语句)语句事件。
3>三个用途
《MySQL技术内幕 InnoDB存储引擎》--恢复、复制、审计。
--恢复
--复制:
主库有一个log dump线程,将binlog传给从库;
从库有两个线程,一个I/O线程,一个SQL线程,I/O线程读取主库传过来的binlog内容并写入到relay log,SQL线程从relay log里面读取内容,写入从库的数据库。
--审计:
用户可以通过二进制日志中的信息来进行审计,判断是否有对数据库进行注入攻击。
3)关联
redo log 和 binlog是怎么关联起来的?
回答:它们有一个共同的数据字段,叫XID。崩溃恢复的时候,会按顺序扫描 redo log:
--如果碰到既有 prepare、又有 commit 的 redo log,就直接提交;
--如果碰到只有 parepare、而没有 commit 的 redo log,就拿着 XID 去 binlog 找对应的事务。
2 redo log
--WAL技术:Write-Ahead Logging,关键点:先写日志,再写磁盘
--具体来说:
① 当有一条记录需要更新,InnoDB 引擎就会先把记录写到redolog,并更新内存,更新完成。同时,InnoDB 引擎会在适当的时候(往往是在系统比较空闲的时候),将这个操作记录更新到磁盘里面。
② redolog大小固定,如,可配置一组 4 个文件,每个文件的大小是 1GB,那总共可以记录 4GB 的操作。从头开始写,写到末尾就又回到开头循环写。
--write pos和checkpoint--循环写
①write pos 当前记录的位置,一边写一边后移,写到第 3 号文件末尾后就回到 0 号文件开头。
②checkpoint 当前要擦除的位置,也是往后推移并且循环的,擦除记录前要把记录更新到数据文件。
关系:writepos、checkpoint 之间空着的部分,可以用来记录新的操作。若writepos 追上checkpoint,表示满了,这时不能再执行新的更新,得停下来先擦掉一些记录,把 checkpoint 推进一下
--crash-safe
redo log使InnoDB保证即使数据库发生异常重启,之前提交的记录都不会丢失,这个能力称为crash-safe;
5 redolog两阶段提交
--两阶段提交:redo log 的写入拆成了两个步骤:prepare 和 commit,这就是"两阶段提交"
--目的:保证2份日志逻辑一致
--反证法证明必要性(update 语句来做例子):
假设当前 ID=2 的行,字段 c 的值是 0,再假设执行update 语句过程中在写完第一个日志后,第二个日志还没有写完期间发生了 crash,出现问题:数据库的状态就有可能和用它的日志恢复出来的库的状态不一致.
① 先写 redo log 后写 binlog
redolog写完crash,redolog存储 binlog没存储,binlog数据恢复丢失数据
② 先写 binlog 后写 redo log
bin写完crash,redolog事务无效没存储,binlog恢复多一个事务
--发生:崩溃(偶尔发生),扩容(常)
3 binlog(归档日志)
--binlog-Server层自己的日志;逻辑日志;追加写的形式
4 执行器和 InnoDB 引擎在执行update的内部流程
--流程图,浅色: InnoDB 内部执行的,深色:执行器中执行的;
1> 查找数据
-执行器先找引擎取 ID=2 这一行。
-ID 是主键,引擎直接用树搜索找到这一行。
-若ID=2 这一行所在的数据页本来就在内存中,就直接返回给执行器;否则,需要先从磁盘读入内存,然后再返回。
2> 赋值
执行器拿到引擎给的行数据,把这个值加上 1,比如原来是 N,现在就是 N+1,得到新的一行数据,再调用引擎接口写入这行新数据。
3> 更新内存,更新redolog
引擎将这行新数据更新到内存中,同时将这个更新操作记录到 redo log 里面,此时redo log 处于 prepare 状态。然后告知执行器执行完成了,随时可以提交事务。
4> 写入binlog
执行器生成这个操作的 binlog,并把 binlog 写入磁盘。
5> 提交事务
执行器调用引擎的提交事务接口,引擎把刚刚写入的 redo log 改成提交(commit)状态,更新完成。
6 数据恢复
怎样让数据库恢复到半个月内任意一秒的状态?
--前提:
-binlog记录所有的逻辑操作,追加写
-备份系统中保存最近半个月的所有binlog,同时系统会定期做整库备份。“定期”取决于系统的重要性,可以是一天一备,也可以是一周一备。
-当需要恢复到指定的某一秒步骤:
① 找到最近一次全量备份,恢复到临时库;
② 从备份的时间点开始,将备份的 binlog 依次取出来,重放到某时间之前的那个时刻。
③ 临时库就跟误删之前的线上库一样,把表数据从临时库取出来,按需要恢复到线上库去。
7 为什么2份日志
为什么会有两份日志呢?因为最开始 MySQL 里并没有 InnoDB 引擎。MySQL 自带的引擎是 MyISAM,但是MyISAM 没有 crash-safe 的能力,binlog 日志只能用于归档。而 InnoDB 是另一个公司以插件形式引入 MySQL 的,既然只依靠 binlog 是没有 crash-safe 能力的,所以 InnoDB使用另外一套日志系统——也就是 redo log 来实现 crash-safe 能力。
8 2份日志的3个不同点
1) redo log 是 InnoDB 引擎特有的;binlog 是 MySQL 的 Server 层实现的,所有引擎都可以使用。
2) redo log 是物理日志,记录的是“在某个数据页上做了什么修改”;binlog 是逻辑日志,记录的是这个语句的原始逻辑,比如“给 ID=2 这一行的 c 字段加 1 ”。
3.)redo log 是循环写的,空间固定会用完;binlog 是可以追加写入的。“追加写”是指binlog 文件写到一定大小后会切换到下一个,并不会覆盖以前的日志。
2 InnoDB 架构
图示看出,innodb主要分为2块:
InnoDB In-Memory Structures 内存
InnoDB On-Disk Structures 磁盘
2.1 InnoDB 内存架构
2)Doublewrite Buffer
--如果说 Change Buffer 是提升性能,那么 Doublewrite Buffer 就是保证数据页的可靠性。
--问题:
MySQL 以「页」为读取和写入单位,一个「页」里面有多行数据,写入数据时,MySQL 会先写内存中的页,然后再刷新到磁盘中的页。
假设在某一次从内存刷新到磁盘的过程中,一个「页」刷了一半,突然操作系统或者 MySQL 进程奔溃了,这时候,内存里的页数据被清除了,而磁盘里的页数据,刷了一半,处于一个中间状态,不尴不尬,可以说是一个「不完整」,甚至是「坏掉的」的页。
-redolog无效:
Redo Log 也已经无力回天,Redo Log 是要在磁盘中的页数据是正常的、没有损坏的情况下,才能把磁盘里页数据 load 到内存,然后应用 Redo Log。
--解决:
MySQL 在刷数据到磁盘之前,要先把数据写到另外一个地方,也就是 Doublewrite Buffer,写完后,再开始写磁盘。Doublewrite Buffer 可以理解为是一个备份(recovery),万一真的发生 crash,就可以利用 Doublewrite Buffer 来修复磁盘里的数据。
1 mysql的存储结构
1)表空间
表空间是数据库中的逻辑结构,它解耦了表、索引等与文件的关联
--概念:表空间是数据库中的逻辑结构,它解耦了表、索引等与文件的关联。
--表空间类型:
1> 系统表空间
存储change buffer, doublewrite buffer以及与innodb相关的所有对象的元数据。(如:表空间和数据库信息,表结构与字段信息等等。)
-mysql8.0中移除原先用于存储表结构信息的.frm文件,所有元数据都存储在此系统表空间中。
2> 独立表空间
-每张表对应一个独立的表空间。
-通过配置my.ini中的参数:innodb_file_per_table=1启动独立表空间,否则,默认为系统表空间。
-5.6.6之后此配置默认开启,因此默认为独立表空间。
-创建时机:
创建表时,会自动为表创建一个对应表名的表空间,并在数据库目录下生成一个“表名.ibd”的表空间文件。
3> 普通表空间
手动创建的表空间:create tablespace 表空间名
4> 临时表空间
存储临时表以及临时表变化对应的回滚段。
2)区/簇/Extent
区是物理存储结构,对应大磁盘中真实的物理空间。它从文件第一个字节开始按相同大小划分,并通过XDES Entry在逻辑上把区串联起来。通过XDES Entry所在页以及页内偏量可以计算出XDES Entry与它管理的物理空间区的关系。
--页:一个磁盘或文件的容量也是非常可观,极其不便管理,因此innodb把文件划分成一个个大小相等的存储块,这些块也被称为页;
--区:
-引入:根据局部性原理,cpu在使用的数据时,下一步也会大概率使用逻辑上相邻的数据。因此为了提高数据读操作的性能,innodb把逻辑上相临的数据尽可能在物理上也存储在相邻的页中;为了实现这一目标,Innodb引入了区/簇的概念;
-概念:一个区/簇是物理上连续分配的一段空间,extent又被划分成连续的页,以存储同一逻辑单元的数据(如下面的索引段、数据段)。一个区/簇,默认由64个连续的页(Page)组成,每个页默认大小为16K。
-作用:为逻辑单元分配连续的空间,同时也用于管理区内的存储空间状态(如:区内哪些页已满,哪些还未使用,哪些包含碎片)
3) 段/Segment
段也是一个逻辑结构,它让具有具体相同逻辑含义和相同存储结构的数据归为一组,方便管理。
--概念:表空间的逻辑组成部分,innodb把逻辑上有关联(具体相同逻辑含义和相同存储结构)的区/簇归属为一个段;
--作用:管理区的使用情况以及为数据分配空间时,提供空间存储状态。
--存储:存储具有相同意义的数据,如:B+对中的非叶子节点或B+树中的叶子节点。常见的段有数据段、索引段、回滚段等。
--创建一个索引就会创建两个段:一个是数据段(B+树对应的叶子节点),一个是索引段(非叶子节点)。
4)页/Page
页是物理存储IO操作的最小单元。它也是从文件第一个字节开始按相同大小划分。表是通过索引的方式组织数据,聚集索引元数据中存储了此表对就的Root page No。页是有编号的,通过编号就可与物理空间建立关联。
--概念:innodb中io操作的最小单位。innodb中的页类似于现实中书本的页。
--大小:
页的大小默认是16KB;可以通过innodb_page_size参数指定,可选项为:4KB、8KB、16KB、32KB、64KB;当page size为4、8、16KB时,对应一个extent的page数量同步变化,以保证extent(区/簇)大小保持1M不变。当page size为32KB或64KB时,extent内的page数量保证不变,extent同步变为2M和4M;
--页号:每个页都有一个对应的从0开始的编号,这个编号叫做页号。
--位置计算:
Innodb引擎可以根据页号和页大小计算出索引B+树root page的准确地址;
--存储内容:
主要用来存储业务相关的数据,以及为了管理内存分配而存在的extent和segment信息;
--page存储内容分类:
① FSP HDR 页
② 任何一个页都由页头、页身和页尾组成
-页头:指明当前页号、类型和所属表空间。
-页尾:主要用于数据的校验。
-页身:这是页中用来存储数据的主要部分。
页身又分为表空间首页头信息区和业务数据区
FSP HEADER包含:1):表空间信息2):段信息3):碎片区/簇信息
5)数据部分
真正的区信息节点则存放在当前页的数据区。
段、区都是为了管理空间的存储状态,为页分配空间服务,真正的查询只需要通过Page No和B+树中各级节点的关联关系就可以操作整个表物理空间上的数据。
2 行/Row
行是最终存储业务数据的物理单元。默认一页16K,可以存储大概1000多行索引数据(非叶子节点),或者20行甚至更多的业务数据(叶子节点)。页之间通过B+树的“二分找查(假设为多分)”算法快速定位数据,页内则通过 Page Directory,把多行分一组,一组对应Page Directory有序数组中的一个slot,这样可以在页内进行一次“二分查找”优化。
--mysql的存储结构是为了给业务数据分配一块用来存储的物理空间,到此终于可以在指定的页中记录业务数据。而innodb是基于行进行存储;
--Compact格式的存储结构中,每条记录都包含一系列头信息,描述当前记录的存储状态。但是除了头信息外,则根据记录所在节点不同存储的数据也有所不同。
-聚集索叶子节点,记录存储的是表中的业务行,除行数据本身外,还包含了事务id,回滚段指针,以及在没有指定主键和唯一索引时还包含一个隐藏的row_id。
-非叶子节点针对的是B+树搜索,因此记录的是子节点的最小记录值以及子节点的页号。
B+树节点与page的关系
--Innodb page只是物理上的存储空间,相当于一本书的一页,仅仅是数据的载体。B+树节点是数据的逻辑结构,理论上它们没有必然的关系。
--Innodb中为了实现简单,B+树节点与page页是一一对应;
为了记录行本身的状态,一条记录innodb会增加额外的记录头信息。如果是叶子节点,还会增加:row_id(隐藏的主键)、trx_id(事务id)、回滚指针等附加字段。
MySQL常用的四种引擎:InnoDB、MyISAM、MEMORY、MERGE存储引擎
2 mysql存储引擎分类
--存储引擎(Storage engine)定义:
MySQL中的数据、索引以及其他对象是如何存储的,是一套文件系统的实现。
-- 数据库存储引擎:是数据库底层软件组织,数据库管理系统(DBMS)使用数据引擎进行创建、查询、更新和删除数据。不同的存储引擎提供不同的存储机制、索引技巧、锁定水平等功能,使用不同的存储引擎,还可以获得特定的功能。现在许多不同的数据库管理系统都支持多种不同的数据引擎。MySQL的核心就是插件式存储引擎。
--常用的存储引擎:
① Innodb引擎:
-- MySQL默认事务型引擎。
在需要它不支持的特性时,才考虑使用其他存储引擎。
-- 提供了对数据库ACID事务的支持。并且还提供了行级锁和外键的约束。
设计的目标就是处理大数据容量的数据库系统。
-- MVCC 来支持高并发,并且实现了四个标准隔离级别(未提交读、提交读、可重复读、可串行化)。其默认级别时可重复读(REPEATABLE READ),在可重复读级别下,通过 MVCC + Next-Key Locking 防止幻读。
-- 主索引时聚簇索引,在索引中保存了数据,从而避免直接读取磁盘,因此对主键查询有很高的性能。
-- 内部做了很多优化,包括从磁盘读取数据时采用的可预测性读,能够自动在内存中创建hash索引以加速读操作的自适应哈希索引,以及能够加速插入操作的插入缓冲区等。
-- 支持真正的在线热备份。
MySQL 其他的存储引擎不支持在线热备份,要获取一致性视图需要停止对所有表的写入,而在读写混合的场景中,停止写入可能也意味着停止读取。
-- 提供了具有提交、回滚和崩溃恢复能力的事务安全。
-- 对比MyISAM引擎,写的处理效率会差一些,并且会占用更多的磁盘空间以保留数据和索引。
-- 特点:支持自动增长列,支持外键约束
② MyIASM引擎
-- mysql5.1及之前版本,为Mysql的默认引擎;
-- 不支持事务,也不支持行级锁和外键。
-- 优势:访问速度快,对事务完整性没有要求或者以select,insert为主的应用基本上可以用这个引擎来创建表;
-- 设计简单,数据以紧密格式存储。对于只读数据,或者表比较小、可以容忍修复操作,则依然可以使用它。
-- 提供了大量的特性,包括压缩表、空间数据索引等。
-- 不支持行级锁,只能对整张表加锁,读取时会对需要读到的所有表加共享锁,写入时则对表加排它锁。但在表有读取操作的同时,也可以往表中插入新的记录,这被称为并发插入(CONCURRENT INSERT)。
-- 可以手工或者自动执行检查和修复操作,但是和事务恢复以及崩溃恢复不同,可能导致一些数据丢失,而且修复操作是非常慢的。
-- 如果指定了 DELAY_KEY_WRITE 选项,在每次修改执行完成时,不会立即将修改的索引数据写入磁盘,而是会写到内存中的键缓冲区,只有在清理键缓冲区或者关闭表的时候才会将对应的索引块写入磁盘。这种方式可以极大的提升写入性能,但是在数据库或者主机崩溃时会造成索引损坏,需要执行修复操作。
-- 支持3种不同的存储格式,分别是:静态表;动态表;压缩表
③ MEMORY引擎:memory
-- 所有的数据都在内存中,数据的处理速度快,但是安全性不高。
-- Memory存储引擎使用存在于内存中的内容来创建表。每个memory表只实际对应一个磁盘文件,格式是.frm。memory类型的表访问非常的快,因为它的数据是放在内存中的,并且默认使用HASH索引,但是一旦服务关闭,表中的数据就会丢失掉。
-- MEMORY存储引擎的表可以选择使用BTREE索引或者HASH索引,两种不同类型的索引有其不同的使用范围
-- Memory类型的存储引擎主要用于哪些内容变化不频繁的代码表,或者作为统计操作的中间结果表,便于高效地对中间结果进行分析并得到最终的统计结果,。对存储引擎为memory的表进行更新操作要谨慎,因为数据并没有实际写入到磁盘中,所以一定要对下次重新启动服务后如何获得这些修改后的数据有所考虑。
④ MERGE存储引擎:merge
-- Merge存储引擎是一组MyISAM表的组合,这些MyISAM表必须结构完全相同,merge表本身并没有数据,对merge类型的表可以进行查询,更新,删除操作,这些操作实际上是对内部的MyISAM表进行的。
3 innodb和MyISAM区别?
区别:
- | Innodb | MyISAM |
---|---|---|
存储结构 | 所有的表都保存在同一个数据文件中(也可能是多个文件,或者是独立的表空间文件),InnoDB表的大小只受限于操作系统文件的大小,一般为2GB | 每张表被存放在三个文件:.frm-表格定义、MYD(MYData)-数据文件、MYI(MYIndex)-索引文件 |
存储空间 | InnoDB的表需要更多的内存和存储,它会在主内存中建立其专用的缓冲池用于高速缓冲数据和索引 | MyISAM可被压缩,存储空间较小 |
可移植性、备份及恢复 | 免费的方案可以是拷贝数据文件、备份 binlog,或者用 mysqldump,在数据量达到几十G的时候就相对痛苦了 | 由于MyISAM的数据是以文件的形式存储,所以在跨平台的数据转移中会很方便。在备份和恢复时可单独针对某个表进行操作 |
文件格式 | 数据和索引是集中存储的,.ibd | 数据和索引是分别存储的,数据.MYD,索引.MYI |
记录存储顺序 | 按主键大小有序插入 | 按记录插入顺序保存 |
外键 | 支持 | 不支持 |
事务 | 支持 | 不支持 |
锁支持(锁是避免资源争用的一个机制,MySQL锁对用户几乎是透明的) | 行级锁定、表级锁定,锁定力度小并发能力高 | 表级锁定 |
SELECT | MyISAM更优 | |
INSERT、UPDATE、DELETE | InnoDB更优 | |
select count(*) | myisam更快,因为myisam内部维护了一个计数器,可以直接调取。 | |
索引的实现方式 | B+树索引,Innodb 是索引组织表 | B+树索引,myisam 是堆表 |
哈希索引 | 支持 | 不支持 |
全文索引 | 不支持 | 支持 |
4 MyISAM索引与InnoDB索引的区别
1)InnoDB索引是聚簇索引,MyISAM索引是非聚簇索引。
2)InnoDB的主键索引的叶子节点存储着行数据,因此主键索引非常高效。
3)MyISAM索引的叶子节点存储的是行数据地址,需要再寻址一次才能得到数据。
4)InnoDB非主键索引的叶子节点存储的是主键和其他带索引的列数据,因此查询时做到覆盖索引会非常高效。
5 存储引擎选择
--是否要支持事务,如果要请选择Innodb,如果不需要可以考虑 MyISAM。
--如果表中绝大多数都是读查询(有人总结出读:写比率大于100:1),可以考虑MyISAM,如果既有读又有写,而且也挺频繁,请使用 InnoDB。
--系统崩溃后,MyISAM 恢复起来更困难,能否接受。
--MySQL 5.5 开始 InnoDB 已经成为 MySQL 的默认引擎(之前是 MyISAM ),说明其优势是有目共睹的,如果你不知道用什么,那就用InnoDB吧,至少不会差。
--如果没有特别的需求,使用默认的Innodb即可。
--MyISAM:以读写插入为主的应用程序,比如博客系统、新闻门户网站。
--Innodb:更新(删除)操作频率也高,或者要保证数据的完整性;并发量高,支持事务和外键。比如OA自动化办公系统。
4 特性之间关系
◆ 只有满足一致性,事务的结果才是正确的。
◆ 在无并发的情况下,事务串行执行,隔离性一定能够满足。此时只要能满足原子性,就一定能满足一致性。在并发的情况下,多个事务并行执行,事务不仅要满足原子性,还需要满足隔离性,才能满足一致性。
◆ 事务满足持久化是为了能应对数据库崩溃的情况。
5 无隔离性:多个事务同时执行的问题
1)脏读
◆ 指在一个事务处理过程里读取了另一个未提交的事务中的数据。
◆ A事务在修改数据未提交,B访问并使用了还未提交的数据
2)不可重复读
◆ 指在对于数据库中的某个数据,一个事务范围内多次查询却返回了不同的数据值,这是由于在查询间隔,被另一个事务修改并提交了。
◆ 不可重复读和脏读的区别是,脏读是某一事务读取了另一个事务未提交的脏数据,而不可重复读则是读取了前一事务提交的数据。
◆ A正多次读取同一行数据,B修改了行数据,导致A多次读取的数据不一致
3)幻读
◆ 幻读是事务非独立执行时发生的一种现象。
◆ 如事务A改变了表某字段的值1—>2,事务B新插入一条某字段仍为1的记录
4)丢失更新
◆ A事务和B事务同时修改同一行数据,出现A事务修改内容丢失,修改内容变成B事务修改内容;
◆ 解决:进行行加锁,只允许并发一个更新事务
6 默认隔离级别
◆ Mysql默认可重读;
◆ Oracle 默认读已提交,
Oracle 迁移到MySQL,将 MySQL 的隔离级别设置为“读提交”,保证数据库隔离级别的一致
7 事务的4个隔离级别及解决问题
--由低到高,级别越高,执行效率就越低
1)read uncommitted(读取未提交):
◆ 概念:一个事务还没提交时,它做的变更就能被别的事务看到。
◆ 任何情况都无法保证,可能会导致脏读、不可重复读或幻读。
2)read committed(读取已提交):
◆ 概念:一个事务提交之后,它做的变更才会被其他事务看到。
◆ 可以阻止脏读,但是幻读或不可重复读仍有可能发生。
3)repeatable read(可重复读):
◆ 概念: 对同一字段的多次读取结果都是一致的,除非数据是被本身事务自己所修改
◆ 可以阻止脏读和不可重复读,但幻读仍有可能发生。
4)serializable(串行化):
◆ 概念:对于同一行记录,“写”会加“写锁”,“读”会加“读锁”,所有的事务依次逐个执行,当出现读写锁冲突的时候,后访问的事务必须等前一个事务执行完成,才能继续执行;
◆ 可以防止脏读、不可重复读以及幻读。
8 可重复读的实现原理
--MySQL每条记录在更新的时候都会同时记录一条回滚操作;
--假设一个值从 1 被按顺序改成了 2、3、4,在回滚日志里面就会有类似下面的记录。
1> 实现:
-当前值是 4,但是在查询这条记录的时候,不同时刻启动的事务会有不同的read-view。如图中看到的,在视图 A、B、C 里面,这一个记录的值分别是1、2、4,同一条记录在系统中可以存在多个版本,就是数据库的多版本并发控制(MVCC)。
-对于 read-viewA,要得到1,就必须将当前值依次执行图中所有的回滚操作得到。
2> 发现:
现在有另外一个事务正在将 4 改成 5,这个事务跟 read-view A、B、C 对应的事务是不会冲突的。
3> 回滚日志删除
-回滚日志总不能一直保留吧,什么时候删除呢?答案是,在不需要的时候才删除。也就是说,系统会判断,当没有事务再需要用到这些回滚日志时,回滚日志会被删除。
-什么时候才不需要了呢?就是当系统里没有比这个回滚日志更早的 read-view 的时候。
4> 长事务
基于上面的说明,我们来讨论一下为什么建议你尽量不要使用长事务。长事务意味着系统里面会存在很老的事务视图。由于这些事务随时可能访问数据库里面的任何数据,所以这个事务提交之前,数据库里面它可能用到的回滚记录都必须保留,这就会导致大量占用存储空间。
示例:在 MySQL 5.5 及以前的版本,回滚日志是跟数据字典一起放在 ibdata 文件里的,即使长事务最终提交,回滚段被清理,文件也不会变小。我见过数据只有 20GB,而回滚段有200GB 的库。最终只好为了清理回滚段,重建整个库。除了对回滚段的影响,长事务还占用锁资源,也可能拖垮整个库(锁部分)
8 可重复读的应用场景
--案例:
假设你在管理一个个人银行账户表。一个表存了每个月月底的余额,一个表存了账单明细。这时候你要做数据校对,也就是判断上个月的余额和当前余额的差额,是否与本月的账单明细一致。你一定希望在校对过程中,即使有用户发生了一笔新的交易,也不影响你的校对结果。
--使用“可重复读”隔离级别,事务启动时的视图可以认为是静态的,不受其他事务更新的影响。
读已提交的原理?
多版本并发控制
MVCC其实就是行级锁的一个升级版。我们都知道数据库中有表锁和行锁,在表锁中读写操作是阻塞的,而MVCC的读写一般是不会阻塞的,这样避免了很多加锁过程。
1)隐藏列
Innodb引擎中数据表会有两个隐藏列,客户端不可见,分别是trx_id,创建版本号;和roll_pointer,回滚指针。
其中创建版本号其实就是创建该行数据的事务id。
2)undo log
事务对数据更新操作,会把旧数据行记录在undo log的记录中,在undo log记录数据行、生成这行数据的事务id。
在undo log中和之前的数据行形成一条链表,链表头就是最新的数据,这条链表就叫做版本链.
事务的可见性都是基于这个undo log来实现的
3)ReadView
查询操作时,事务会生成一个ReadView,ReadView是一个事务快照,准确来说是当前时间点系统内活跃的事务列表,也就是说系统内所有未提交的事务,都会记录在这个Readview内,事务就根据它来判断哪些数据是可见的,哪些是不可见的。
查询一条数据时,事务会拿到这个ReadView,去到undo log中进行判断。若查询到某一条数据:
先去查看undo log中的最新数据行,如果数据行的版本号小于ReadView记录的事务id最小值,就说明这条数据对当前数据库是可见的,可以直接作为结果集返回
若数据行版本号大于ReadView记录最大值,说明这条数据是由一个新的事务修改的,对当前事务不可见,那么就顺着版本链继续往下寻找第一条满足条件的
若数据行版本号在ReadView最小值和最大值之间,那么就需要进行遍历了整个ReadView了,如果数据行版本号等于ReadView的某个值,说说明该行数据仍然处于活跃状态,那么对当前事务不可见
读已提交和可重复读的实现
ReadView就是这样来判断数据可见性的。
那又是如何实现读已提交和可重复读呢?其实很简单,就是生成ReadView的时机不同。
对读已提交来说,事务中的每次读操作都会生成一个新的ReadView,也就是说,如果这期间某个事务提交了,那么它就会从ReadView中移除。这样确保事务每次读操作都能读到相对比较新的数据
而对可重复读来说,事务只有在第一次进行读操作时才会生成一个ReadView,后续的读操作都会重复使用这个ReadView。也就是说,如果在此期间有其他事务提交了,那么对于可重复读来说也是不可见的,因为对它来说,事务活跃状态在第一次进行读操作时就已经确定下来,后面不会修改了。
https://blog.csdn.net/SCUTJAY/article/details/104653599
9 事务的启动方式
--MySQL 的事务启动几种方式:
① 显式启动事务语句, begin/start transaction。配套的提交语句是 commit,回滚语句是 rollback。
② set autocommit=0,这个命令会将这个线程的自动提交关掉。
如,只执行一个 select 语句,这个事务就启动了,而且并不会自动提交。需要你主动执行 commit 或 rollback 语句,或者断开连接。
--set autocommit=0问题:
如果长连接,会导致意外的长事务;
建议:使用set autocommit=1, 通过显式语句的方式来启动事务。
--使用:autocommit = 1 的情况
用 begin 显式启动的事务,执行 commit 则提交事务。
执行 commit work and chain,则是提交事务并自动启动下一个事务。不需要主动执行一次 “begin”,减少了语句的交互次数。优点:rd明确地知道每个语句是否处于事务中;
设置隔离级别
启动参数 transaction-isolation 的值设置成 READ-COMMITTED
比较读提交,重复读性能
https://www.cnblogs.com/hainange/p/6153632.html
1 undo log
--2个作用:回滚和多版本控制(MVCC)
--写:数据修改时,不仅记录redolog,还记录undolog,某些原因导致事务失败或回滚,可用undo log回滚;
--写的例子:相反记录,insert->delete
undolog主要存储的是逻辑日志,如insert一条数据了,则undo log会记录的一条对应的delete日志。相反的对应记录。
--原子性:一个事务包含多个操作,这些操作要么全部执行,要么全都不执行
实现:undolog,与修改的操作相反的记录,达到回滚,保证一致性。
--undo log存储着修改之前的数据,相当于一个前版本,MVCC实现的是读写不阻塞,读的时候只要返回前一个版本的数据就行了。
2 事务的启动时机
无特别说明,默认autocommit=1;
--M1:begin/start transaction
此命名非事务的起点,在执行到它们之后的第一个操作InnoDB 表的语句,事务才真正启动。
--M2:transaction with consistent snapshot
可以马上启动一个事务;
--第一种启动方式,一致性视图是在第执行第一个快照读语句时创建的;第二种启动方式,一致性视图是在执行 start transaction with consistentsnapshot 时创建的
3 2个视图概念
1)view。
是一个用查询语句定义的虚拟表,在调用的时候执行查询语句并生成结果。创建视图的语法是 create view ... ,而它的查询方法与表一样。
2)一致性读视图
一致性读视图(consistent read view)是InnoDB 在实现 MVCC 时用到的,用于支持 RC(Read Committed,读提交)和 RR(Repeatable Read,可重复读)隔离级别的实现。
4 MVCC里的快照实现-版本链
--可重复读隔离级别下,事务在启动的时候就“拍了个快照”。注,这个快照是基于整库的。
--快照的实现:
每行数据也都是有多个版本的。每次事务更新数据的时候,都会生成一个新的数据版本,并且把 transaction id 赋值给这个数据版本的事务 ID,记为 row trx_id。同时,旧的数据版本要保留,并且在新的数据版本中,能够有信息可以直接拿到它。
即,数据表中的一行记录,其实可能有多个版本 (row),每个版本有自己的 row trx_id。
其中,
transaction id:
InnoDB 里面每个事务有一个唯一的事务 ID,叫作 transaction id。它是在事务开始的时候向 InnoDB 的事务系统申请的,是按申请顺序严格递增的。
row trx_id:隐藏列
roll_pointer:隐藏列。每次对某条聚簇索引记录进行改动时,都会把旧的版本写入到undo日志中,然后这个隐藏列就相当于一个指针,可以通过它来找到该记录修改前的信息
--undolog的应用:
undo log的回滚机制也是依靠这个版本链,每次对记录进行改动,都会记录一条undo日志,每条undo日志也都有一个roll_pointer属性(INSERT操作对应的undo日志没有该属性,因为该记录并没有更早的版本),可以将这些undo日志都连起来,串成一个链表,所以现在的情况就像下图一样:
例子
如,一个记录被多个事务连续更新后的状态:
图中的三个虚线箭头,就是 undo log;
V1、V2、V3 并不是物理上真实存在的,而是每次需要的时候根据当前版本和 undo log 计算出来的。
如,需要 V2 的时候,就是通过 V4 依次执行 U3、U2 算出来。
5 如何定义快照-row trx_id的可见性
--问题:100G的库,生成快照,不需要拷贝出这 100G的数据?
--可重复读:根据定义,事务启动的时刻为准,如果一个数据版本是在这启动之前生成的,就认;如果是这启动以后才生成的,我就不认,我必须要找到它的上一个版本;以及自身更新也认;
--一致性视图(read-view)原理:
InnoDB 为每个事务构造了一个数组,用来保存这个事务启动瞬间,当前正活跃(启动还未提交)的所有事务 ID。
① 数组中低水位:事务 ID 的最小值;
高水位:当前系统里面已经创建过的事务 ID 的最大值+1。
② 视图数组和高水位,就组成了当前事务的一致性视图(read-view)。
③ 数据版本的可见性规则:
基于数据的 row trx_id 和这个一致性视图的对比结果得到的。
--视图数组把所有的 row trx_id 分成了几种不同的情况。
对于当前事务的启动瞬间来说,一个数据版本的 row trx_id,有以下几种可能:
总结
InnoDB 利用了“所有数据都有多个版本”的这个特性,实现了“秒级创建快照”的能力。
6 可重复读是怎么实现的?_ab
可重复读,在第一次读取数据时生成一个ReadView,对于使用REPEATABLE READ隔离级别的事务来说,只会在第一次执行查询语句时生成一个ReadView,之后的查询就不会重复生成了,所以一个事务的查询结果每次都是一样的。
读已提交_ab
--在读提交隔离级别下,这个视图是在每个SQL语句开始执行的时候创建的,在这个隔离级别下,事务在每次查询开始时都会生成一个独立的ReadView。
区别
而读提交的逻辑和可重复读的逻辑类似,它们最主要的区别是:
--在可重复读隔离级别下,只需要在事务开始的时候创建一致性视图,之后事务里的其他查询都共用这个一致性视图;
--在读提交隔离级别下,每一个语句执行前都会重新算出一个新的视图。
MVCC只在REPEATABLE READ和READ COMMITTED两个隔离级别下工作。其他两个隔离级别都和MVCC不兼容(4),因为READ UNCOMMITTED总是读取最新的数据行,而不是符合当前事务版本的数据行。而SERIALIZABLE则会对所有读取的行都加锁。
快照读与当前读
◆ 在可重复读级别中,通过MVCC机制,虽然让数据变得可重复读,但我们读到的数据可能是历史数据,是不及时的数据,不是数据库当前的数据!这在一些对于数据的时效特别敏感的业务中,就很可能出问题。
◆ 对于这种读取历史数据的方式,我们叫它快照读 (snapshot read),而读取数据库当前版本数据的方式,叫当前读 (current read)。很显然,在MVCC中:
① 快照读
MVCC 的 SELECT 操作是快照中的数据,不需要进行加锁操作。
select * from table ….;
② 当前读
MVCC 其它会对数据库进行修改的操作(INSERT、UPDATE、DELETE)需要进行加锁操作,从而读取最新的数据。可以看到 MVCC 并不是完全不用加锁,而只是避免了 SELECT 的加锁操作。
INSERT;
UPDATE;
DELETE;
在进行 SELECT 操作时,可以强制指定进行加锁操作。以下第一个语句需要加 S 锁,第二个需要加 X 锁。
- select * from table where ? lock in share mode;
- select * from table where ? for update;
◆ 事务的隔离级别实际上都是定义的当前读的级别,MySQL为了减少锁处理(包括等待其它锁)的时间,提升并发能力,引入了快照读的概念,使得select不用加锁。而update、insert这些“当前读”的隔离性,就需要通过加锁来实现了。
RC和RR级别事务的实现:一致性视图、MVCC
6 可重复读是怎么实现的?_ab
可重复读,在第一次读取数据时生成一个ReadView,对于使用REPEATABLE READ隔离级别的事务来说,只会在第一次执行查询语句时生成一个ReadView,之后的查询就不会重复生成了,所以一个事务的查询结果每次都是一样的。(SELECT都会复用这个 Read view)
区别
而读提交的逻辑和可重复读的逻辑类似,它们最主要的区别是:
--在可重复读隔离级别下,只需要在事务开始的时候创建一致性视图,之后事务里的其他查询都共用这个一致性视图;
--在读提交隔离级别下,每一个语句执行前都会重新算出一个新的视图。
说一下你了解的锁
排他锁、共享锁、意向锁
记录锁、间隙锁、临键锁
乐观锁、悲观锁、
自增锁
行锁、表锁
加锁-首先要知道DB加了那些锁 --锁监控
作用:满足事务隔离性 保证并发
锁怎么看
show enfine innodb status\G 当前存储引擎的状态
完整的:(此处有其他操作)
show profile for query 1 (1 为id)
show profiles
慢慢被淘汰
替换为:perfomance_schema(开发可以稍微忽略先)
87张表,监控信息
2 全局锁
--概念:全局锁就是对整个数据库实例加锁。让整个库处于只读状态。
--命令:Flush tables with read lock (FTWRL)
--典型使用场景:做全库逻辑备份。
--备份不加锁导致问题:不能拿到一致性视图
--备份解决:
① 可重复读隔离级别
可重复读隔离级别下开启一个事务。
官方自带的逻辑备份工具是 mysqldump。当 mysqldump 使用参数–single-transaction的时候,导数据之前就会启动一个事务,来确保拿到一致性视图。而由于 MVCC 的支持,这个过程中数据是可以正常更新的。
---single-transaction 方法只适用于所有的表使用事务引擎的库
② 全局锁(FTWRL)
---用于:对于 MyISAM 这种不支持事务的引擎,如果备份过程中有更新,总是只能取到最新的数据,那么就破坏了备份的一致性。
③ 为什么不用 set global readonly=true?
一是,在有些系统中,readonly 的值会被用来做其他逻辑,比如用来判断一个库是主库还是备库。因此,修改 global 变量的方式影响面更大,不建议使用。
二是,在异常处理机制上有差异。执行 FTWRL 命令之后由于客户端发生异常断开,那么 MySQL 会自动释放这个全局锁,整个库回到可以正常更新的状态。而将整个库设置为 readonly 之后,如果客户端发生异常,则数据库就会一直保持 readonly 状态,这样会导致整个库长时间处于不可写状态,风险较高。
3 表级锁
3)MDL
--MDL 不需要显式使用,在访问一个表的时候会被自动加上。
--作用:保证读写的正确性。
--在 MySQL 5.5 版本中引入了 MDL,当对一个表做增删改查操作的时候,加 MDL读锁;当要对表做结构变更操作的时候,加 MDL 写锁。
--读锁之间不互斥,读写锁之间、写锁之间是互斥的(针对一张表)
--缺陷:
事务中的 MDL 锁,在语句执行开始时申请,但是语句结束后并不会马上释放,而会等到整个事务提交后再释放。
频繁提交,导致不释放,这个库的线程会爆满
4 行锁
4)热点行更新导致的性能问题
--问题:
假设有 1000 个并发线程要同时更新同一行,那么死锁检测操作就是100 万这个量级的。虽然最终检测的结果是没有死锁,但是这期间要消耗大量的 CPU 资源。因此,你就会看到 CPU 利用率很高,但是每秒却执行不了几个事务。
---问题的症结在于,死锁检测要耗费大量的 CPU 资源。
---一种头痛医头的方法,就是如果你能确保这个业务一定不会出现死锁,可以临时把死锁检测关掉。有风险
---另一个思路是控制并发度。
这个并发控制要做在数据库服务端。如果你有中间件,可以考虑在中间件实现;如果你的团队有能修改 MySQL 源码的人,也可以做在 MySQL 里面。基本思路就是,对于相同行的更新,在进入引擎之前排队。这样在 InnoDB 内部就不会有大量的死锁检测工作了。
简化,可以考虑通过将一行改成逻辑上的多行来减少锁冲突。
1 锁分类
--锁分类:按锁的粒度
行级锁(INNODB引擎)、表级锁(MYISAM引擎)和页级锁(BDB引擎)
--存储引擎锁使用:
◆ MyISAM采用表级锁(table-level locking)。
◆ InnoDB支持行级锁(row-level locking)和表级锁,默认为行级锁
--锁的粒度取决于具体的存储引擎,InnoDB实现了行级锁,页级锁,表级锁。
加锁开销从大到小,并发能力也是从大到小。
行级锁,表级锁和页级锁对比
1)行级锁 **
◆ 行级锁是Mysql中锁定粒度最细的一种锁,表示只针对当前操作的行进行加锁。行级锁能大大减少数据库操作的冲突。其加锁粒度最小,但加锁的开销也最大。行级锁分为共享锁 和 排他锁。
◆ 特点:开销大,加锁慢;会出现死锁;锁定粒度最小,发生锁冲突的概率最低,并发度也最高。
**2)表级锁
◆ 表级锁是MySQL中锁定粒度最大的一种锁,表示对当前操作的整张表加锁,它实现简单,资源消耗较少,被大部分MySQL引擎支持。最常使用的MYISAM与INNODB都支持表级锁定。表级锁定分为表共享读锁(共享锁)与表独占写锁(排他锁)。
◆ 特点:开销小,加锁快;不会出现死锁;锁定粒度大,发出锁冲突的概率最高,并发度最低。
3)页级锁
页级锁是MySQL中锁定粒度介于行级锁和表级锁中间的一种锁。表级锁速度快,但冲突多,行级冲突少,但速度慢。所以取了折衷的页级,一次锁定相邻的一组记录。
◆ 特点:开销和加锁时间界于表锁和行锁之间;会出现死锁;锁定粒度界于表锁和行锁之间,并发度一般
3 锁的类别上分MySQL锁分类
--从锁的类别上来讲,有共享锁和排他锁。
--共享锁: 又叫做读锁。 当用户要进行数据的读取时,对数据加上共享锁。共享锁可以同时加上多个。
--排他锁: 又叫做写锁。 当用户要进行数据的写入时,对数据加上排他锁。排他锁只可以加一个,他和其他的排他锁,共享锁都相斥。
--用上面的例子来说就是用户的行为有两种,一种是来看房,多个用户一起看房是可以接受的。 一种是真正的入住一晚,在这期间,无论是想入住的还是想看房的都不可以。
4 InnoDB存储引擎的三种锁算法
◆ Record lock:单个行记录上的锁 -记录锁
-- 锁定一个记录上的索引,而不是记录本身。
-- 如果表没有设置索引,InnoDB 会自动在主键上创建隐藏的聚簇索引,因此 Record Locks 依然可以使用。
◆ Gap lock:间隙锁,锁定一个范围,不包括记录本身
-- 锁定索引之间的间隙,但是不包含索引本身。例如当一个事务执行以下语句,其它事务就不能在 t.c 中插入 15。
-- SELECT c FROM t WHERE c BETWEEN 10 and 20 FOR UPDATE;
◆ Next-key lock:record+gap 锁定一个范围,包含记录本身
-- 它是 Record Locks 和 Gap Locks 的结合,不仅锁定一个记录上的索引,也锁定索引之间的间隙。
-- next-key锁其实包含了记录锁和间隙锁,即锁定一个范围,并且锁定记录本身,InnoDB默认加锁方式是next-key 锁。
-- 例如一个索引包含以下值:10, 11, 13, and 20,那么就需要锁定以下区间:
(-∞, 10]
(10, 11]
(11, 13]
(13, 20]
(20, +∞)
在 InnoDB 存储引擎中,SELECT 操作的不可重复读问题通过 MVCC 得到了解决,而 UPDATE、DELETE 的不可重复读问题通过 Record Lock 解决,INSERT的不可重复读问题是通过 Next-Key Lock(Record Lock + Gap Lock)解决的。
4 使用实现
1)行锁?
--MySQL中InnoDB引擎基于索引来完成行锁
-- 例: select * from tab_with_index where id = 1 for update;
for update 可以根据条件来完成行锁锁定,并且 id 是有索引键的列,如果 id 不是索引键那么InnoDB将完成表锁,并发将无从谈起
比如有一个表child,id列上有90,100,102,
-- 当我们执行select * from chlid where id=100 for update 时,mysql会锁住90到102这个区间,一开始有点疑惑就是其实mysql只需要去锁定id=100这个值就可以防止幻读了,为什么还要去锁定相邻的区间范围呢?
这是为了预防另一种情况的发生。
-- 比如当我们执行 select * from chlid where id>100 for update时,这时next-key锁就派上用场了。
索引扫描到了100和102这两个值,但是仅仅锁住这两个值是不够的,因为当我们在另一个会话插入id=101的时候,就有可能产生幻读了。
所以mysql必须锁住[100,102)和[102,无穷大)这个范围,才能保证不会出现幻读。
5 隔离级别与锁的关系
-- 在Read Uncommitted级别下,读取数据不需要加共享锁,这样就不会跟被修改的数据上的排他锁冲突
-- 在Read Committed级别下,读操作需要加共享锁,但是在语句执行完以后释放共享锁;
--在Repeatable Read级别下,读操作需要加共享锁,但是在事务提交之前并不释放共享锁,也就是必须等待事务执行完毕以后才释放共享锁。
-- SERIALIZABLE 是限制性最强的隔离级别,因为该级别锁定整个范围的键,并一直持有锁,直到事务完成。
6 死锁以及解决
--死锁是指两个或多个事务在同一资源上相互占用,并请求锁定对方的资源,从而导致恶性循环的现象。
--常见的解决死锁的方法
1)如果不同程序会并发存取多个表,尽量约定以相同的顺序访问表,可以大大降低死锁机会。
2)在同一个事务中,尽可能做到一次锁定所需要的所有资源,减少死锁产生概率;
3)对于非常容易产生死锁的业务部分,可以尝试使用升级锁定颗粒度,通过表级锁定来减少死锁产生的概率;
如果业务处理不好可以用分布式事务锁或者使用乐观锁
乐观锁和悲观锁,实现
两种锁的使用场景
◆ 从上面对两种锁的介绍,我们知道两种锁各有优缺点,不可认为一种好于另一种,像乐观锁适用于写比较少的情况下(多读场景),即冲突真的很少发生的时候,这样可以省去了锁的开销,加大了系统的整个吞吐量。
◆ 但如果是多写的情况,一般会经常产生冲突,这就会导致上层应用会不断的进行retry,这样反倒是降低了性能,所以一般多写的场景下用悲观锁就比较合适。
页的概念:
大小与操作系统有关,一般4k、8k,4的倍数,innodb默认16k(具体一页有多大数据跟操作系统有关);读取一页内的数据时候,实际上才发生了一次IO。
二分查找的复杂度:O(log2n)
1 二叉树
二叉查找树的特点就是左子树的节点值比父亲节点小,而右子树的节点值比父亲节点大:
-- 在查找某个节点的时候,可以采取类似于二分查找的思想,快速找到某个节点。n个节点的二叉查找树,正常的情况下,查找的时间复杂度为 O(logn)。
-- 保证每次查找都可以这折半而减少IO次数;
极端情况:之所以说是正常情况下,是因为二叉查找树有可能出现一种极端的情况:
-- 此时的二叉查找树已经近似退化为一条链表,查找时间复杂度顿时变成了O(n)。由此必须防止这种情况发生,为了解决这个问题,于是引申出了平衡二叉树。
2 平衡二叉树
1)概念
平衡二叉树是基于二分法的策略提高数据的查找速度的二叉树的数据结构。
-- 平衡二叉树是采用二分法思维,平衡二叉查找树除了具备二叉树的特点,最主要的特征是树的左右两个子树的层级最多相差1。在插入删除数据时通过左旋/右旋操作保持二叉树的平衡,不会出现左子树很高、右子树很矮的情况。
-- 平衡二叉查找树查询的性能接近于二分查找法,时间复杂度是O(log2n)。
2)规则
平衡二叉树是采用二分法思维把数据按规则组装成一个树形结构的数据,用这个树形结构的数据减少无关数据的检索,大大的提升了数据检索的速度;平衡二叉树的数据结构组装过程有以下规则:
① 非叶子节点只能允许最多两个子节点存在。
② 每一个非叶子节点数据分布规则为左边的子节点小当前节点的值,右边的子节点大于当前节点的值(这里值是基于自己的算法规则而定的,比如hash值)。
平衡树的层级结构:
平衡二叉树的查询性能和树的层级(高度h)成反比,h值越小查询越快。为了保证树的结构左右两端数据大致平衡。降低二叉树的查询难度一般会采用一种算法机制实现节点数据结构的平衡,实现了这种算法的有比如Treap、红黑树。使用平衡二叉树能保证数据的左右两边的节点层级相差不会大于1,通过这样避免树形结构由于删除增加变成线性链表影响查询效率,保证数据平衡的情况下查找数据的速度近于二分法查找:
3)平衡二叉树特点:
① 非叶子节点最多拥有两个子节点。
② 非叶子节点值大于左边子节点、小于右边子节点。
③ 树的左右两边的层级数相差不会大于1。
④ 没有值相等重复的节点。
适用场景:
平衡二叉树,一般是用平衡因子差值决定并通过旋转来实现,左右子树树高差不超过1,那么和红黑树比较,它是严格的平衡二叉树,平衡条件非常严格(树高差只有1),只要插入或删除不满足上面的条件就要通过旋转来保持平衡。由于旋转是非常耗费时间的。所以 AVL树适用于插入/删除次数比较少,但查找多的场景。
--存在问题:
① 时间复杂度和树高相关。
树有多高就需要检索多少次,每个节点的读取,都对应一次磁盘 IO 操作。树的高度就等于每次查询数据时磁盘 IO 操作的次数。磁盘每次寻道时间为10ms,在表数据量大时,查询性能就会很差。(1百万的数据量,log2n约等于20次磁盘IO,时间20*10=0.2s)
② 平衡二叉树不支持范围查询快速查找,范围查询时需要从根节点多次遍历,查询效率不高。
3 红黑树
1)为什么有了平衡树还需要红黑树?
虽然平衡树解决了二叉查找树退化为近似链表的缺点,能够把查找时间控制在O(logn),不过却不是最佳的,因为平衡树要求每个节点的左子树和右子树的高度差至多等于1,这个要求实在是太严了,导致每次进行插入/删除节点的时候,几乎都会破坏平衡树的第二个规则,进而都需要通过左旋和右旋来进行调整,使之再次成为一颗符合要求的平衡树。
2)红黑树的特性
显然,如果在插入、删除很频繁的场景中,平衡树需要频繁调整,这会使平衡树的性能大打折扣,为了解决这个问题,于是有了红黑树,红黑树具有如下特点:
① 每个节点或者是黑色,或者是红色。
② 根节点是黑色。
③ 每个叶子节点(NIL)是黑色。 [注意:这里叶子节点,是指为空(NIL或NULL)的叶子节点]
④ 如果一个节点是红色的,则它的子节点必须是黑色的。
⑤ 从一个节点到该节点的子孙节点的所有路径上包含相同数目的黑节点。[这里指到叶子节点的路径]
-- 包含n个内部节点的红黑树的高度是 O(log(n))。如图:
3)红黑树的使用场景
-- java中使用到红黑树的有TreeSet和JDK1.8的HashMap。红黑树的插入和删除都要满足以上5个特性,操作非常复杂,为什么要使用红黑树?
原因:红黑树是一种平衡树,复杂的定义和规则都是为了保证树的平衡性。如果树不保证平衡性就是下图:很显然这就变成一个链表了。
-- 保证平衡性的最大的目的就是降低树的高度,因为树的查找性能取决于树的高度。所以树的高度越低搜索的效率越高!
通过对从根节点到叶子节点路径上各个节点的颜色进行约束,确保没有一条路径会比其他路径长2倍,因而是近似平衡的。所以相对于严格要求平衡的AVL树来说,它的旋转保持平衡次数较少。适合,查找少,插入/删除次数多的场景。(现在部分场景使用跳表来替换红黑树,可搜索“为啥 redis 使用跳表(skiplist)而不是使用 red-black?”)
改造二叉树:为什么引入B树
MySQL的数据是存储在磁盘文件中的,查询处理数据时,需要先把磁盘中的数据加载到内存中,磁盘IO操作非常耗时,所以优化的重点就是尽量减少磁盘IO操作。访问二叉树的每个节点就会发生一次IO,如果想要减少磁盘IO操作,就需要尽量降低树的高度。那如何降低树的高度呢?
--假如key为bigint=8字节,每个节点有两个指针,每个指针为4个字节,一个节点占用的空间16个字节(8+4*2=16)。
--因为在MySQL的InnoDB存储引擎一次IO会读取的一页(默认一页16K)的数据量,而二叉树一次IO有效数据量只有16字节,空间利用率极低。为了最大化利用一次IO空间,一个简单的想法是在每个节点存储多个元素,在每个节点尽可能多的存储数据。每个节点可以存储1000个索引(16k/16=1000),这样就将二叉树改造成了多叉树,通过增加树的叉树,将树从高瘦变为矮胖。构建1百万条数据,树的高度只需要2层就可以(1000*1000=1百万),也就是说只需要2次磁盘IO就可以查询到数据。磁盘IO次数变少了,查询数据的效率也就提高了。
--这种数据结构我们称为B树,B树是一种多叉平衡查找树
4 B树(B-tree)
B树和B-tree,其实是同一种树。
--B树的定义
B树(Balance Tree)也称B-树,它是一颗多路平衡查找树。我们描述一颗B树时需要指定它的阶数,阶数表示了一个结点最多有多少个孩子结点,一般用字母m表示阶数。当m取2时,就是我们常见的二叉搜索树。
一颗m阶的B树定义如下:
1)每个结点最多有m-1个关键字。
2)根结点最少可以只有1个关键字。
3)非根结点至少有Math.ceil(m/2)-1个关键字。
4)每个结点中的关键字都按照从小到大的顺序排列,每个关键字的左子树中的所有关键字都小于它,而右子树中的所有关键字都大于它。
5)所有叶子结点都位于同一层,或者说根结点到每个叶子结点的长度都相同。
-- 图示:一棵阶数为4的B树。
阶数m:在实际应用中的B树的阶数m都非常大(通常大于100),所以即使存储大量的数据,B树的高度仍然比较小。
节点:每个结点中存储了关键字(key)和关键字对应的数据(data),以及孩子结点的指针。将一个key和其对应的data称为一个记录。但为了方便描述,除非特别说明,后续文中就用key来代替(key,value)键值对这个整体。在数据库中我们将B树(和B+树)作为索引结构,可以加快查询速速,此时B树中的key就表示键,而data表示了这个键对应的条目在硬盘上的逻辑地址。
1)概念
-- 与平衡二叉树稍有不同的是,B树属于多叉树又名平衡多路查找树(查找路径不只两个),数据库索引技术里大量使用B树和B+树的数据结构。
-- 主要特点:
① B树的节点中存储着多个元素,每个内节点有多个分叉。
② 节点中的元素包含键值和数据,节点中的键值从大到小排列。也就是说,在所有的节点都储存数据。
③ 父节点当中的元素不会出现在子节点中。
④ 所有的叶子结点都位于同一层,叶节点具有相同的深度,叶节点之间没有指针连接。
2)规则
① 排序方式:所有节点关键字是按递增次序排列,并遵循左小右大原则。
② 子节点数:非叶子节点的子节点数>1,且<=M,且M>=2,空树除外(注:M阶代表一个树节点最多有多少个查找路径,M=M路,当M=2则是2叉树,M=3则是3叉)。
③ 关键字数:枝节点的关键字数量大于等于ceil(m/2)-1个且小于等于M-1个(注:ceil()是个朝正无穷方向取整的函数。如ceil(1.1)结果为2)。
④ 所有叶子节点均在同一层、叶子节点除了包含了关键字和关键字记录的指针外也有指向其子节点的指针只不过其指针地址都为null对应下图最后一层节点的空格子。
3)b树查询数据的流程:
-- 假如我们查询值等于10的数据。查询路径磁盘块1->磁盘块2->磁盘块5。
① 第一次磁盘IO:将磁盘块1加载到内存中,在内存中从头遍历比较,10<15,走左路,到磁盘寻址磁盘块2。
② 第二次磁盘IO:将磁盘块2加载到内存中,在内存中从头遍历比较,7<10,到磁盘中寻址定位到磁盘块5。
③ 第三次磁盘IO:将磁盘块5加载到内存中,在内存中从头遍历比较,10=10,找到10,取出data,如果data存储的行记录,取出data,查询结束。如果存储的是磁盘地址,还需要根据磁盘地址到磁盘中取出数据,查询终止。
-- 相比二叉平衡查找树:
在整个查找过程中,虽然数据的比较次数并没有明显减少,但是磁盘IO次数会大大减少。同时,由于我们的比较是在内存中进行的,比较的耗时可以忽略不计。B树的高度一般2至3层就能满足大部分的应用场景,所以使用B树构建索引可以很好的提升查询的效率。
-- B树索引查询过程:
4)️B树的插入节点流程
定义一个5阶树(平衡5路查找树),现在要把3、8、31、11、23、29、50、28这些数字构建出一个5阶树出来。遵循规则:
①节点拆分规则:当前是要组成一个5路查找树,那么此时m=5,关键字数必须<=5-1(这里关键字数>4就要进行节点拆分)。
②排序规则:满足节点本身比左边节点大,比右边节点小。
5)B树节点的删除
规则:
①节点合并规则:当前是要组成一个5路查找树,那么此时m=5,关键字数必须大于等于ceil(5/2)(这里关键字数<2就要进行节点合并)。
②满足节点本身比左边节点大,比右边节点小的排序规则。
③关键字数小于二时先从子节点取,子节点没有符合条件时就向父节点取,取中间值往父节点放。
特点:
B树相对于平衡二叉树的不同是,每个节点包含的关键字增多了,特别是在B树应用到数据库中的时候,数据库充分利用了磁盘块的原理(磁盘数据存储是采用块的形式存储的,每个块的大小为4K,每次IO进行数据读取时,同一个磁盘块的数据可以一次性读取出来)把节点大小限制和充分使用在磁盘快大小范围;把树的节点关键字增多后树的层级比原来的二叉树少了,减少数据查找的次数和复杂度。
b树的缺点:
1)B树不支持范围查询的快速查找,你想想这么一个情况如果我们想要查找10和35之间的数据,查找到15之后,需要回到根节点重新遍历查找,需要从根节点进行多次遍历,查询效率有待提高。
2)如果data存储的是行记录,行的大小随着列数的增多,所占空间会变大。这时,一个页中可存储的数据量就会变少,树相应就会变高,磁盘IO次数就会变大。
6 B+树
1)概念
-- B+ Tree 是 B 树的一种变形、升级,它是基于 B Tree 和叶子节点顺序访问指针进行实现,通常用于数据库和操作系统的文件系统中。B+树更充分的利用了节点的空间,让查询速度更加稳定,其速度完全接近于二分法查找。
-- B+ 树有两种类型的节点:内部节点(也称索引节点)和叶子节点,内部节点就是非叶子节点,内部节点不存储数据,只存储索引,数据都存在叶子节点。
-- 内部节点中的 key 都按照从小到大的顺序排列,对于内部节点中的一个 key,左子树中的所有 key 都小于它,右子树中的 key 都大于等于它,叶子节点的记录也是按照从小到大排列的。
-- 每个叶子节点都存有相邻叶子节点的指针。
-- B+树和B树最主要的区别在于非叶子节点是否存储数据的问题:
B树:非叶子节点和叶子节点都会存储数据。
B+树:只有叶子节点才会存储数据,非叶子节点至存储键值。叶子节点之间使用双向指针连接,最底层的叶子节点形成了一个双向有序链表。
-- B+树的最底层叶子节点包含了所有的索引项。
B+树在查找数据的时候,由于数据都存放在最底层的叶子节点上,所以每次查找都需要检索到叶子节点才能查询到数据。
-- 所以在需要查询数据的情况下每次的磁盘的IO跟树高有直接的关系,但是从另一方面来说,由于数据都被放到了叶子节点,放索引的磁盘块锁存放的索引数量是会跟这增加的,相对于B树来说,B+树的树高理论上情况下是比B树要矮的。
-- 也存在索引覆盖查询的情况,
在索引中数据满足了当前查询语句所需要的全部数据,此时只需要找到索引即可立刻返回,不需要检索到最底层的叶子节点。
2)规则
① B+跟B树不同。
B+树的非叶子节点不保存关键字记录的指针,只进行数据索引,这样使得B+树每个非叶子节点所能保存的关键字大大增加。
② B+树叶子节点保存了父节点的所有关键字记录的指针,所有数据地址必须要到叶子节点才能获取到。所以每次数据查询的次数都一样。
③ B+树叶子节点的关键字从小到大有序排列,左边结尾数据都会保存右边节点开始数据的指针。
④ 非叶子节点的子节点数=关键字数(百度百科。根据各种资料,这里有两种算法的实现方式,另一种为非叶节点的关键字数=子节点数-1(维基百科),虽然数据排列结构不一样,但其原理还是一样的。Mysql 的 B+树是用第一种方式实现)。
3)特点
① B+树的层级更少:
相较于B树B+每个非叶子节点存储的关键字数更多,树的层级更少所以查询数据更快。
② B+树查询速度更稳定:
B+所有关键字数据地址都存在叶子节点上,所以每次查找的次数都相同所以查询速度要比B树更稳定。
③ B+树天然具备排序功能:
B+树所有的叶子节点数据构成了一个有序链表,在查询大小区间的数据时候更方便,数据紧密性很高,缓存的命中率也会比B树高。
④ B+树全节点遍历更快:
B+树遍历整棵树只需要遍历所有的叶子节点即可,而不需要像B树对每一层进行遍历,这有利于数据库做全表扫描。
⑤ B树相对于B+树的优点是,
如果经常访问的数据离根节点很近,而B树的非叶子节点本身存有关键字其数据的地址,所以这种数据检索的时候会要比B+树快。
等值查询:
假如我们查询值等于9的数据。查询路径磁盘块1->磁盘块2->磁盘块6。
① 第一次磁盘IO:将磁盘块1加载到内存中,在内存中从头遍历比较,9<15,走左路,到磁盘寻址磁盘块2。
② 第二次磁盘IO:将磁盘块2加载到内存中,在内存中从头遍历比较,7<9<12,到磁盘中寻址定位到磁盘块6。
③ 第三次磁盘IO:将磁盘块6加载到内存中,在内存中从头遍历比较,在第三个索引中找到9,取出data,如果data存储的行记录,取出data,查询结束。如果存储的是磁盘地址,还需要根据磁盘地址到磁盘中取出数据,查询终止。(这里需要区分的是在InnoDB中Data存储的为行数据,而MyIsam中存储的是磁盘地址。)
范围查询:
假如我们想要查找9和26之间的数据。查找路径是磁盘块1->磁盘块2->磁盘块6->磁盘块7。
① 首先查找值等于9的数据,将值等于9的数据缓存到结果集。这一步和前面等值查询流程一样,发生了三次磁盘IO。
② 查找到15之后,底层的叶子节点是一个有序列表,我们从磁盘块6,键值9开始向后遍历筛选所有符合筛选条件的数据。
③ 第四次磁盘IO:根据磁盘6后继指针到磁盘中寻址定位到磁盘块7,将磁盘7加载到内存中,在内存中从头遍历比较,9<25<26,9<26<=26,将data缓存到结果集。
④ 主键具备唯一性(后面不会有<=26的数据),不需再向后查找,查询终止。将结果集返回给用户。
可以看到B+树可以保证等值和范围查询的快速查找,MySQL的索引就采用了B+树的数据结构。
插入过程:5阶B树的插入
5阶B数的结点最少2个key,最多4个key。
a)空树中插入5
b)依次插入8,10,15
c)插入16
插入16后超过了关键字的个数限制,所以要进行分裂。在叶子结点分裂时,分裂出来的左结点2个记录,右边3个记录,中间key成为索引结点中的key,分裂后当前结点指向了父结点(根结点)。结果如下图所示。
当然我们还有另一种分裂方式,给左结点3个记录,右结点2个记录,此时索引结点中的key就变为15。
d)插入17
e)插入18,插入后如下图所示
当前结点的关键字个数大于5,进行分裂。分裂成两个结点,左结点2个记录,右结点3个记录,关键字16进位到父结点(索引类型)中,将当前结点的指针指向父结点。
当前结点的关键字个数满足条件,插入结束。
f)插入若干数据后
g)在上图中插入7,结果如下图所示
当前结点的关键字个数超过4,需要分裂。左结点2个记录,右结点3个记录。分裂后关键字7进入到父结点中,将当前结点的指针指向父结点,结果如下图所示。
当前结点的关键字个数超过4,需要继续分裂。左结点2个关键字,右结点2个关键字,关键字16进入到父结点中,将当前结点指向父结点,结果如下图所示。
当前结点的关键字个数满足条件,插入结束。
6 B*树
1)规则
B*树是B+树的变种,区别如下:
①首先是关键字个数限制问题,B+树初始化的关键字初始化个数是ceil(m/2),B*树的初始化个数为ceil(2/3*m)。
②B+树节点满时就会分裂,而B*树节点满时会检查兄弟节点是否满(因为每个节点都有指向兄弟的指针),如果兄弟节点未满则向兄弟节点转移关键字,如果兄弟节点已满,则从当前节点和兄弟节点各拿出1/3的数据创建一个新的节点出来。
2)特点
在B+树的基础上因其初始化的容量变大,使得节点空间使用率更高,而又存有兄弟节点的指针,可以向兄弟节点转移关键字的特性使得B*树额分解次数变得更少;
7 总结
1)相同思想和策略
从平衡二叉树、B树、B+树、B*树总体来看它们的贯彻的思想是相同的,都是采用二分法和数据平衡策略来提升查找数据的速度。
2) 不同的方式的磁盘空间利用
不同点是它们一个一个在演变的过程中通过IO从磁盘读取数据的原理进行一步步的演变,每一次演变都是为了让节点的空间更合理的运用起来,从而使树的层级减少达到快速查找数据的目的。
3 数据存储的选择
1)hash
--hash存储的问题:
① 需要优良的hash算法解决哈希碰撞、哈希冲突
② 无序,无法进行范围查询:在大表扫描时,效率低
③ 需要大量的内存空间:如扩容等
--但hash仍有使用:
Memory存储引擎支持hash索引,innodb支持自适应hash。
2)b+树
1> 二叉树
二叉问题:树结构过深,则IO次数变多
优点:有序
2> b树
多阶,b树缺点:无重复数据
3> 存储
--存储比较:
(16k 1000=1024换字节,一层的存储量16000byte)
b树:假设data为1k,三层数据量 16x16x16=4096
b+树:假设占用kv10个字节 16x1000/10=1600,三层:1600x1600x16=40960000
4> b+树
--b+树是在b树的基础之前做的一种优化,变化如下:
① b+树每个节点可以包含更多的节点,这个做的原因有两个,第一个原因是为了降低树的高度,第二个原因是将数据范围变为多个区间,区间越多,数据检索越快。
② 非叶子节点存储key,叶子节点存储key和数据
③ 叶子节点两两指针互相连接(符合磁盘预读特性),顺序查询性能更高
--b+:重复数据->叶子包含全部数据->非叶子不存数据
--数据存储量:假设占用kv10个字节 16x1000/10=1600
--问:索引一般几层?:标准回答 3-4层足以支撑千万级数据量
块大小不变 key值变化
int 4字节
varchar 一般超过4或10
-- 叶子节点有序
int id自增 ?为什么?防止页分裂
页合并-麻烦
--索引的维护
大量插入 性能变低
Hash索引和B+树有什么区别或者说优劣呢? √
首先要知道Hash索引和B+树索引的底层实现原理:
-- hash索引底层就是hash表,进行查找时,调用一次hash函数就可以获取到相应的键值,之后进行回表查询获得实际数据。B+树底层实现是多路平衡查找树。对于每一次的查询都是从根节点出发,查找到叶子节点方可以获得所查键值,然后根据查询判断是否需要回表查询数据。
-- Hash表,在Java中的HashMap,TreeMap就是Hash表结构,以键值对的方式存储数据。我们使用Hash表存储表数据Key可以存储索引列,Value可以存储行记录或者行磁盘地址。Hash表在等值查询时效率很高,时间复杂度为O(1);但是不支持范围快速查找,范围查找时还是只能通过扫描全表方式。
显然这种并不适合作为经常需要查找和范围查找的数据库索引使用。
你都是如何设计索引的?
https://mp.weixin.qq.com/s/-gmAPfiKMNJgHhIZqR2C4A|用对了这些场景下的索引
• InnoDB 存储引擎有一个特殊的功能叫“自适应哈希索引”,当某个索引值被使用的非常频繁时,会在 B+Tree 索引之上再创建一个哈希索引,这样就让 B+Tree 索引具有哈希索引的一些优点,比如快速的哈希查找。
索引使用场景(重点)[-]
-- 增加一个没有建立索引的字段
-- (alter table 表名 add index(字段名))
alter table innodb1 add sex char(1);
-- 按sex检索时可选的索引为null
EXPLAIN SELECT * from innodb1 where sex='男';
索引为什么用在很多值重复的字段上会失效
是优化器的选择,如果比例太高,mysql会认为这种方式很低效,因为涉及到回表,所以默认全表扫描
使用索引查询一定能提高查询的性能吗?为什么 [-]
2)可用性优先策略
--流程:
强行把步骤 4、5 调整到最开始执行,即不等主备数据同步,直接把连接切到备库 B,并且让备库 B 可以读写,那么系统这个不可用时间几乎降为 0。
--问题:
能出现数据不一致的情况。
--建表:
create table t('id','c'...
增主键 id,初始化,主库和备库上都是 3 行数据
insert into t(c) values(1),(2),(3);
//rd执行:
insert into t(c) values(4);
insert into t(c) values(5);
现在主库上其他的数据表有大量的更新,导致主备延迟达到 5 秒。在插入一条 c=4的语句后,发起了主备切换。
--情况1:
可用性优先策略,且 binlog_format=mixed时的切换流程和数据结果:
1步骤 2 中,主库 A 执行完 insert 语句,插入了一行数据(4,4),之后开始进行主备切换。
2步骤 3 中,由于主备之间有 5 秒的延迟,所以备库 B 还没来得及应用“插入 c=4”这个中转日志,就开始接收客户端“插入 c=5”的命令。
3步骤 4 中,备库 B 插入了一行数据(4,5),并且把这个 binlog 发给主库 A。
4步骤 5 中,备库 B 执行“插入 c=4”这个中转日志,插入了一行数据(5,4)。而直接在备库 B 执行的“插入 c=5”这个语句,传到主库 A,就插入了一行新数据(5,5)。
--情况2:
可用性优先策略,但设置 binlog_format=row
---row 格式在记录 binlog 的时候,会记录新插入的行的所有字段值,所以最后只会有一行不一致。而且,两边的主备同步的应用线程会报错 duplicate key error 并停止。也就是说,这种情况下,备库 B 的 (5,4) 和主库 A 的 (5,5) 这两行数据,都不会被对方执行。
--结论:
1> 使用 row 格式的 binlog 时,数据不一致的问题更容易被发现。而使用 mixed 或者statement 格式的 binlog 时,数据很可能悄悄地就不一致了。
2>主备切换的可用性优先策略会导致数据不一致。因此,大多数情况下,建议使用可靠性优先策略。
3 小时级备库延时
待补充.......
复制过程
Binary log:主数据库的二进制日志
Relay log:从服务器的中继日志
第一步:master在每个事务更新数据完成之前,将该操作记录串行地写入到binlog文件中。
第二步:salve开启一个I/O Thread,该线程在master打开一个普通连接,主要工作是binlog dump process。如果读取的进度已经跟上了master,就进入睡眠状态并等待master产生新的事件。I/O线程最终的目的是将这些事件写入到中继日志中。
第三步:SQL Thread会读取中继日志,并顺序执行该日志中的SQL事件,从而与主数据库中的数据保持一致。
mysql主从复制主要有哪几种模式?
mysql主从同步怎么做?
mysql是集群还是单节点?最大连接数,最大的表中数据量大约是多少?
1 读写分离基本概念
--目的:
目标就是分摊主库的压力;
结构1:客户端直连架构
一主多从的结构,就是读写分离的基本结构了:
客户端(client)主动做负载均衡,这种模式下一般会把数据库的连接信息放在客户端的连接层。
结构2:带 proxy 的读写分离架构
MySQL 和客户端之间有一个中间代理层 proxy,客户端只连接proxy, 由 proxy 根据请求类型和上下文决定请求的分发路由。
--两种方案的优劣:
趋势是往带 proxy 的架构方向发展的
1> 客户端直连方案:
因为少了一层 proxy 转发,所以查询性能稍微好一点儿,并且整体架构简单,排查问题更方便。但是这种方案,由于要了解后端部署细节,所以在出现主备切换、库迁移等操作的时候,客户端都会感知到,并且需要调整数据库连接信息。你可能会觉得这样客户端也太麻烦了,信息大量冗余,架构很丑。其实也未必,一般采用这样的架构,一定会伴随一个负责管理后端的组件,比如 Zookeeper,尽量让业务端只专注于业务逻辑开发。
2> 带 proxy 的架构:
对客户端比较友好。客户端不需要关注后端细节,连接维护、后端信息维护等工作,都是由 proxy 完成的。但这样的话,对后端维护团队的要求会更高。而且,proxy 也需要有高可用架构。因此,带 proxy 架构的整体就相对比较复杂
2 主备延迟导致的问题
--问题:
两种架构都存在:由于主从可能存在延迟,客户端执行完一个更新事务后马上发起查询,如果查询选择的是从库的话,就有可能读到刚刚的事务更新之前的状态。即“在从库上会读到系统的一个过期状态”的现象。
--主从延迟还是不能 100% 避免的
6)GTID 方案
读写分离常用代理方式来实现,代理服务器接收应用层传来的读写请求,然后决定转发到哪个服务器。
sql查询2?
sql查询2?
常用的函数?
Mysql如何拼接字符串?
1)CONCAT(string1,string2,…)
说明 : string1,string2代表字符串,concat函数在连接字符串的时候,只要其中一个是NULL,那么将返回NULL
2)CONCAT_WS(separator,str1,str2,...)
说明 : string1,string2代表字符串,concat_ws 代表 concat with separator,第一个参数是其它参数的分隔符。分隔符的位置放在要连接的两个字符串之间。分隔符可以是一个字符串,也可以是其它参数。如果分隔符为 NULL,则结果为 NULL。函数会忽略任何分隔符参数后的 NULL 值。
3)group_concat函数
完整的语法如下:
group_concat([DISTINCT] 要连接的字段 [Order BY ASC/DESC 排序字段] [Separator '分隔符'])
Mysql去重关键字?
in如何实现的?
数据库中JOIN是怎么实现的?
mysql的几种连接?
左连接、内连接、右连接
right join原理
左外连接和内连接的区别? |2
1)内连接,显示两个表中有联系的所有数据;
2)左链接,以左表为参照,显示所有数据;
3)右链接,以右表为参照显示数据;
https://www.cnblogs.com/cs071122/p/6753681.html
为什么要使用数据库? [-]
为什么在技术选型时选择MySQL,而不是选择Oracle?
数据库三范式?
mysql的三种驱动类型?
1)Class.forName("com.mysql.jdbc.Driver");//加载数据库驱动
2)new com.mysql.jdbc.Driver() ;//创建driver对象,加载数据库驱动
https://www.iteye.com/blog/862123204-qq-com-1566581
写SQL的注意事项?
https://thinkwon.blog.csdn.net/article/details/104778621
1. 慢查询如何分析排查和优化? |3
mysql如何优化(回答索引、拆分等) |5
mysql查询优化?
索引、关联子查询等,最常见的就是给表加上合适的索引
mysql在项目中的优化场景?
分库分表的理解,好处
https://blog.csdn.net/u010817136/article/details/51037845
数据库垂直与水平拆分怎么做?
分库分表数据切分
sql注入原理及解决方案?
https://www.cnblogs.com/jiaoxiaohui/p/10760763.html
资料:
为什么大家都说SELECT * 效率低 - 老刘的文章 - 知乎
https://zhuanlan.zhihu.com/p/149981715
资料:★
https://thinkwon.blog.csdn.net/article/details/104778621
https://thinkwon.blog.csdn.net/article/details/104778621?utm_medium=distribute.pc_relevant.none-task-blog-OPENSEARCH-7.control&depth_1-utm_source=distribute.pc_relevant.none-task-blog-OPENSEARCH-7.control
https://mp.weixin.qq.com/s/J3kCOJwyv2nzvI0_X0tlnA
数据库mysql索引? |6
索引作用?
数据库索引的优缺点?
索引的优势和劣势?
索引的优点
索引优点
Mysql索引的坏处是什么?
1)创建索引和维护索引要耗费时间,这种时间随着数据量的增加而增加。
2)索引需要占物理空间,除了数据表占数据空间之外,每一个索引还要占一定的物理空间,如果要建立聚簇索引,那么需要的空间就会更大。
3)当对表中的数据进行增加、删除和修改的时候,索引也要动态的维护,这样就降低了数据的维护速度。
https://blog.csdn.net/kennyrose/article/details/7532032
InnoDB引擎的4大特性[-]
插入缓冲(insert buffer)
二次写(double write)
自适应哈希索引(ahi)
预读(read ahead)
索引,为什么选择自增?
索引模型是什么?
索引的底层/mysql索引数据结构? |3
索引原理?
数据库的索引原理 ???
通常是「平衡树」(非二叉),也就是b tree及其变种B+树。
https://blog.csdn.net/kennyrose/article/details/7532032
https://blog.csdn.net/z_ryan/article/details/79685072 √
https://www.cnblogs.com/harderman-mapleleaves/p/4528212.html
https://www.cnblogs.com/makai/p/10861296.html
https://www.cnblogs.com/aspwebchh/p/6652855.html
MySQL索引数据结构? |4
索引算法有哪些? [-]
BTree算法
BTree是最常用的mysql数据库索引算法,也是mysql默认的算法。因为它不仅可以被用在=,>,>=,<,<=和between这些比较操作符上,而且还可以用于like操作符,只要它的查询条件是一个不以通配符开头的常量, 例如:
-- 只要它的查询条件是一个不以通配符开头的常量
select * from user where name like 'jack%';
-- 如果一通配符开头,或者没有使用常量,则不会使用索引,例如:
select * from user where name like '%jack';
Hash算法
Hash Hash索引只能用于对等比较,例如=,<=>(相当于=)操作符。由于是一次定位数据,不像BTree索引需要从根节点到枝节点,最后才能访问到页节点这样多次IO访问,所以检索效率远高于BTree索引。
??
索引是在存储引擎中实现的,而不是在服务器层中实现的。所以,每种存储引擎的索引都不一定完全相同,并不是所有的存储引擎都支持所有的索引类型。
InnoDB和MyISAM
1)InnoDB
主键索引与非主键索引有什么区别 ?
什么是聚簇索引?何时使用聚簇索引与非聚簇索引 [-]
聚簇索引和非聚簇索引?
高性能的索引策略
1 聚簇索引(Clustered Indexes)
https://www.cnblogs.com/whgk/p/6179612.html
https://www.cnblogs.com/likeju/p/5409102.html
2 聚簇索引和非聚簇索引
聚簇索引和非聚簇索引的区别?
答了聚簇索引:结构、建立(主键上建立、无主键则选择第一个唯一索引,若都没有主键和唯一索引则隐藏有一个字段实现聚簇索引)
非聚簇结构、
非聚簇索引一定会回表查询吗?[-]
B+树在满足聚簇索引和覆盖索引的时候不需要回表查询数据,[-]
-- 在B+树的索引中,叶子节点可能存储了当前的key值,也可能存储了当前的key值以及整行的数据,这就是聚簇索引和非聚簇索引。 在InnoDB中,只有主键索引是聚簇索引,如果没有主键,则挑选一个唯一键建立聚簇索引。如果没有唯一键,则隐式的生成一个键来建立聚簇索引。
-- 当查询使用聚簇索引时,在对应的叶子节点,可以获取到整行数据,因此不用再次进行回表查询。
Mysql回表?回表问题?
回表的过程,磁盘读几次,跟数据在内存中比哪个快?
联合索引是什么?为什么需要注意联合索引中的顺序?[-]
Mysql对联合索引有优化么?会自动调整顺序么?哪个版本开始优化?
前缀索引 [-]
什么是最左前缀原则?什么是最左匹配原则 [-]
联合索引的最左匹配原则? |2
(答了:建立多列索引、多列索引顺序性和索引下推)
从底层解释最左匹配原则?
mysql存储引擎索引优化?
mysql索引有哪些,都有什么特点?
索引的分类/索引有哪几种类型?
索引的分类?
mytable表:
CREATE TABLE mytable(
ID INT NOT NULL,
username VARCHAR(16) NOT NULL,
city VARCHAR(50) NOT NULL,
age INT NOT NULL
);
1)单例索引
一个索引只包含单个列,但一个表中可以有多个单列索引。
① 普通索引
没有什么限制,允许在定义索引的列中插入重复值和空值。
◆ 创建1:创建索引
CREATE INDEX indexName ON mytable(username(length));
-- 如果是CHAR,VARCHAR类型,length可以小于字段实际长度;如果是BLOB和TEXT类型,必须指定 length,下同。
◆ 创建2:修改表结构
ALTER mytable ADD INDEX [indexName] ON (username(length))
◆ 创建3:创建表的时候直接指定
CREATE TABLE mytable(:
ID INT NOT NULL,
username VARCHAR(16) NOT NULL,
INDEX [indexName] (username(length))
);
◆ 删除索引的语法:
DROP INDEX [indexName] ON mytable;
② 唯一索引
索引列中的值必须是唯一的,但是允许为空值。
◆ 创建索引
CREATE UNIQUE INDEX indexName ON mytable(username(length))
◆ 修改表结构
ALTER mytable ADD UNIQUE [indexName] ON (username(length))
◆ 创建表的时候直接指定
CREATE TABLE mytable(
ID INT NOT NULL,
username VARCHAR(16) NOT NULL,
UNIQUE [indexName] (username(length))
);
③ 主键索引
是一种特殊的唯一索引,不允许有空值。
◆ 创建:一般是在建表的时候同时创建主键索引:
CREATE TABLE mytable(
ID INT NOT NULL,
username VARCHAR(16) NOT NULL,
PRIMARY KEY(ID)
);
当然也可以用 ALTER 命令。记住:一个表只能有一个主键。
2) 组合索引
在表中的多个字段组合上创建的索引,只有在查询条件中使用了这些字段的左边字段时,索引才会被使用,使用组合索引时遵循最左前缀集合。
◆ 创建:将 name, city, age建到一个索引里:
ALTER TABLE mytable ADD INDEX name_city_age (name(10),city,age);
◆ 使用:“最左前缀”都会用到
usernname,city,age | usernname,city | usernname
SELECT * FROM mytable WHREE username="admin" AND city="郑州"
SELECT * FROM mytable WHREE username="admin"
3) 全文索引
在一堆文字中,通过其中的某个关键字等,就能找到该字段所属的记录行,比如有"你是个大煞笔,二货 ..." 通过大煞笔,可能就可以找到该条记录。
只有在MyISAM引擎上才能使用,只能在CHAR,VARCHAR,TEXT类型字段上使用全文索引。
FULLTEXT
mysql索引类型?
单列索引(普通索引,唯一索引,主键索引)、组合索引、全文索引、空间索引
索引之间的区别
1) 单列索引:一个索引只包含单个列,但一个表中可以有多个单列索引。
怎么建立索引1?
select * from t where b=1;
Select * from t where a=1 and b=1;
先说需要建两个索引,后来反应过来了,建一个联合索引。
怎么建索引2?
select * from a=1 and b>2 or c in(1,2,3)
场景题:音乐界面和评论,如何建立表和索引
Select * from t where c=1;
C是非主键索引,问几次磁盘io,b+索引树高度3。
mysql给性别建立索引 和 直接查询 有区别吗?
只有一个字段,字段值都是汉字,建立索引后是如何排序的 ?
索引:A>0 B =3 C=1 会不会走索引?
一列只有8中情况的数据,另一列不确认,哪一列适合建索引?
索引语句
CREATE TABLE table_name
[col_name data type]
[unique|fulltext]
[index|key]
[index_name](col_name[length])
[asc|desc]
怎么建索引? |2
https://www.cnblogs.com/whgk/p/6179612.html
创建索引的三种方式,删除索引? [-]
第一种方式:在执行CREATE TABLE时创建索引
CREATE TABLE user_index2 (
id INT auto_increment PRIMARY KEY,
first_name VARCHAR (16),
last_name VARCHAR (16),
id_card VARCHAR (18),
information text,
KEY name (first_name, last_name),
FULLTEXT KEY (information),
UNIQUE KEY (id_card)
);
第二种方式:使用ALTER TABLE命令去增加索引
ALTER TABLE table_name ADD INDEX index_name (column_list);
-- ALTER TABLE用来创建普通索引、UNIQUE索引或PRIMARY KEY索引。
-- 其中,table_name是要增加索引的表名,column_list指出对哪些列进行索引,多列时各列之间用逗号分隔。
-- 索引名index_name可自己命名,缺省时,MySQL将根据第一个索引列赋一个名称。另外,ALTER TABLE允许在单个语句中更改多个表,因此可以在同时创建多个索引。
CREATE INDEX index_name ON table_name (column_list);
-- CREATE INDEX可对表增加普通索引或UNIQUE索引。(但是,不能创建PRIMARY KEY索引)
alter table user_index drop KEY name;
alter table user_index drop KEY id_card;
alter table user_index drop KEY information;
-- 删除主键索引:alter table 表名 drop primary key(因为主键只有一个)。这里值得注意的是,如果主键自增长,那么不能直接执行此操作(自增长依赖于主键索引):
-- 需要取消自增长再行删除:
-- 但通常不会删除主键,因为设计主键一定与业务逻辑无关。
alter table user_index
-- 重新定义字段
MODIFY id int,
drop PRIMARY KEY
百万级别或以上的数据如何删除 [-]
创建索引的原则(重中之重)
建索引时需要注意什么? [-]
MySQL建立索引有什么规则 ?
索引的使用注意事项
1)哪些情况下不需要使用索引
2)索引不可用的情况
3)索引不会被使用的几种情况
https://www.cnblogs.com/xyhero/p/b0ad525c6a6a5ed2bd7f40918c5dbd98.html
使用原则:
1、对经常更新的表就避免对其进行过多的索引,对经常用于查询的字段应该创建索引,
2、数据量小的表最好不要使用索引,因为由于数据较少,可能查询全部数据花费的时间比遍历索引的时间还要短,索引就可能不会产生优化效果。
3、在一同值少的列上(字段上)不要建立索引,比如在学生表的"性别"字段上只有男,女两个不同值。相反的,在一个字段上不同值较多可是建立索引
索引的使用条件? [-]
为什么对于非常小的表,大部分情况下简单的全表扫描比建立索引更高效?
索引设计的原则?[-]
适合索引的列是出现在where子句中的列,或者连接子句中指定的列
基数较小的类,索引效果较差,没有必要在此列建立索引
使用短索引,如果对长字符串列进行索引,应该指定一个前缀长度,这样能够节省大量索引空间
不要过度索引。索引需要额外的磁盘空间,并降低写操作的性能。在修改表内容的时候,索引会进行更新甚至重构,索引列越多,这个时间就会越长。所以只保持需要的索引有利于查询即可。
哪些建立索引比较适合(比如性别建立索引合适吗)
索引是建立在数据库表中的某些列的上面。在创建索引的时候,应该考虑在哪些列上可以创建索引,在哪些列上不能创建索引。一般来说,应该在这些列上创建索引:在经常需要搜索的列上,可以加快搜索的速度;在作为主键的列上,强制该列的唯一性和组织表中数据的排列结构;在经常用在连接的列上,这些列主要是一些外键,可以加快连接的速度;在经常需要根据范围进行搜索的列上创建索引,因为索引已经排序,其指定的范围是连续的;在经常需要排序的列上创建索引,因为索引已经排序,这样查询可以利用索引的排序,加快排序查询时间;在经常使用在WHERE子句中的列上面创建索引,加快条件的判断速度。
https://blog.csdn.net/kennyrose/article/details/7532032
https://zhuanlan.zhihu.com/p/48337244
https://wiki.jikexueyuan.com/project/redis/lua.html
|aobing|
redis的缓存穿透、缓存击穿、缓存雪崩原因现象和解决措施? |3
redis缓存穿透与解决措施? |3 (rky
String get(String key) {
// 从缓存中获取数据
String cacheValue = cache.get(key);
// 缓存为空
if (StringUtils.isBlank(cacheValue)) {
// 从存储中获取
String storageValue = storage.get(key);
cache.set(key, storageValue);
// 如果存储数据为空,需要设置一个过期时间(300秒)
if (storageValue == null) {
cache.expire(key, 60 * 5);
}
return storageValue;
} else {
// 缓存非空
return cacheValue;
}
}
2)布隆过滤器拦截
► 如图,在访问缓存层和存储层之前,将存在的key用布隆过滤器提前保存起来,做第一层拦截。
► 场景:
例如:一个推荐系统有4亿个用户id,每个小时算法工程师会根据每个用户之前历史行为计算出推荐数据放到存储层中,但是最新的用户由于没有历史行为,就会发生缓存穿透的行为,为此可以将所有推荐数据的用户做成布隆过滤器。如果布隆过滤器认为该用户id不存在,那么就不会访问存储层,在一定程度保护了存储层。
► 实现:
有关布隆过滤器的相关知识,可以参考:https://en.wikipedia.org/wiki/Bloom_filter可以利用Redis的Bitmaps实现布隆过滤器,GitHub上已经开源了类似的方案,读者可以进行参考:https://github.com/erikdubbelboer/redis-lua-scaling-bloom-filter。
► 应用场景:
适用于数据命中不高、数据相对固定、实时性低(通常是数据集较大)的应用场景,代码维护较为复杂,但是缓存空间占用少。
► 两种解决方法的对比(实际上这个问题是一个开放问题,有很多解决方法)
redis缓存雪崩与解决措施? |3 (rky
见你写了个加随机数预防缓存雪崩,解释一下?
redis缓存击穿(热点数据集中失效/热点key重建优化)与解决措施? |3 (rky
String get(String key) {
// 从Redis中获取数据
String value = redis.get(key);
// 如果value为空,则开始重构缓存
if (value == null) {
// 只允许一个线程重构缓存,使用nx,并设置过期时间ex
String mutexKey = "mutext:key:" + key;
if (redis.set(mutexKey, "1", "ex 180", "nx")) {
// 从数据源获取数据
value = db.get(key);
// 回写Redis,并设置过期时间
redis.setex(key, timeout, value);
// 删除key_mutex
redis.delete(mutexKey);
}
// 其他线程休息50毫秒后重试
else {
Thread.sleep(50);
get(key);
}
}
return value;
}
1)从Redis获取数据,如果值不为空,则直接返回值;否则执行下面的2.1)和2.2)步骤。
2.1)如果set(nx和ex)结果为true,说明此时没有其他线程重建缓存,那么当前线程执行缓存构建逻辑。
2.2)如果set(nx和ex)结果为false,说明此时已经有其他线程正在执行构建缓存的工作,那么当前线程将休息指定时间(例如这里是50毫秒,取决于构建缓存的速度)后,重新执行函数,直到获取到数据。
2)永远不过期
► “永远不过期”包含两层意思:
-- 从缓存层面来看,确实没有设置过期时间,所以不会出现热点key过期后产生的问题,也就是“物理”不过期。
-- 从功能层面来看,为每个value设置一个逻辑过期时间,当发现超过逻辑过期时间后,会使用单独的线程去构建缓存。
► 从实战看,此方法有效杜绝了热点key产生的问题,但唯一不足的就是重构缓存期间,会出现数据不一致的情况,这取决于应用方是否容忍这种不一致。
► 代码实现:
String get(final String key) {
V v = redis.get(key);
String value = v.getValue();
// 逻辑过期时间
long logicTimeout = v.getLogicTimeout();
// 如果逻辑过期时间小于当前时间,开始后台构建
if (v.logicTimeout <= System.currentTimeMillis()) {
String mutexKey = "mutex:key:" + key;
if (redis.set(mutexKey, "1", "ex 180", "nx")) {
// 重构缓存
threadPool.execute(new Runnable() {
public void run() {
String dbValue = db.get(key);
redis.set(key, (dbvalue,newLogicTimeout));
redis.delete(mutexKey);
}
});
}
}
return value;
}
为什么选择Redis作为缓存?
-- 收益:
① 加速读写:因为缓存通常都是全内存的(例如Redis、Memcache),而存储层通常读写性能不够强悍(例如MySQL),通过缓存的使用可以有效地加速读写,优化用户体验。
② 降低后端负载:帮助后端减少访问量和复杂计算(例如很复杂的SQL语句),在很大程度降低了后端的负载。
缓存一致性相关问题?
设计一个缓存商品的方案,什么时候保存商品到缓存,什么时候删除缓存的商品?
如何设计一个秒杀系统?
① 怎么测试秒杀
② Redis怎么库存预热,RabbitMQ怎么进行队列削峰
如何解决一个高并发场景呢?
(答数据库主从复制读写分离,分库分表,服务器划分不通服务或者负载均衡,加消息队列和缓存)
Redis项目中用来做什么 ?
你项目如果用redis改进,怎么改?
1 经典的缓存+数据库读写的模式,Cache Aside Pattern
--读的时候,先读缓存,缓存没有的话,就读数据库,然后取出数据后放入缓存,同时返回响应。
--更新的时候,先更新数据库,然后再删除缓存。
为什么是删除缓存,而不是更新缓存?
--数据不一致问题:故先更新数据库,再删缓存
1)请求A进行写操作,删除缓存2)请求B查询发现缓存不存在3)请求B去数据库查询得到旧值4)请求B将旧值写入缓存5)请求A将新值写入数据库
2 高并发的优化
--问题:A查询,B更新->脏数据
1)缓存刚好失效(2)请求A查询数据库,得一个旧值(3)请求B将新值写入数据库(4)请求B删除缓存(5)请求A将查到的旧值写入缓存
--优化:异步延时删除策略/缓存设置有效时间
我的理解:更新数据时,发送到一个队列中。读取数据的时候,如果发现数据不在缓存中,重新执行“读取数据+更新缓存”的操作,也发送到同一个队列中。串行执行队列中的。过滤重复的更新请求。
基础知识点:
redis缓存回收机制?
因为C语言并不具备自动内存回收功能,所以Redis在自己的对象系统中构建了一个引用计数(reference counting)技术实现的内存回收机制,通过这一机制,程序可以通过跟踪对象的引用计数信息,在适当的时候自动释放对象并进行内存回收。
每个对象的引用计数信息由redisObject结构的refcount属性记录:
typedef struct redisObject {
// ...
// 引用计数
int refcount;
// ...
} robj;
对象的引用计数信息会随着对象的使用状态而不断变化:
·在创建一个新对象时,引用计数的值会被初始化为1;
·当对象被一个新程序使用时,它的引用计数值会被增一;
·当对象不再被一个程序使用时,它的引用计数值会被减一;
·当对象的引用计数值变为0时,对象所占用的内存会被释放。
生命周期
对象的整个生命周期可以划分为创建对象、操作对象、释放对象三个阶段。作为例子,以下代码展示了一个字符串对象从创建到释放的整个过程(其他不同类型的对象也会经历类似的过程):
// 创建一个字符串对象s,对象的引用计数为1
robj *s = createStringObject(...)
//对象s执行各种操作...
// 将对象s 的引用计数减一,使得对象的引用计数变为0
// 导致对象s 被释放
decrRefCount(s)
API
修改对象引用计数的API,这些API分别用于增加、减少、重置对象的引用计数。
什么是内存碎片,产生的原因?
redis数据达到多少是阈值?
redis最大内存设置了多少?
redis为什么要设置过期时间?
过期时间是怎么设置的?
redis key 的过期键删除策略?
定期删除怎么实现的,是开启一个新进程还是停止工作去删除?
整个过程可以用伪代码描述如下:
# 默认每次检查的数据库数量
DEFAULT_DB_NUMBERS = 16
# 默认每个数据库检查的键数量
DEFAULT_KEY_NUMBERS = 20
# 全局变量,记录检查进度
current_db = 0
def activeExpireCycle():
# 初始化要检查的数据库数量
# 如果服务器的数据库数量比DEFAULT_DB_NUMBERS要小,那么以服务器的数据库数量为准
if server.dbnum < DEFAULT_DB_NUMBERS:
db_numbers = server.dbnum
else:
db_numbers = DEFAULT_DB_NUMBERS
# 遍历各个数据库
for i in range(db_numbers):
# 如果current_db的值等于服务器的数据库数量,这表示检查程序已经遍历了服务器的所有数据库一次
# 将current_db重置为0,开始新的一轮遍历
if current_db == server.dbnum:
current_db = 0
# 获取当前要处理的数据库
redisDb = server.db[current_db]
# 将数据库索引增1,指向下一个要处理的数据库
current_db += 1
# 检查数据库键
for j in range(DEFAULT_KEY_NUMBERS):
# 如果数据库中没有一个键带有过期时间,那么跳过这个数据库
if redisDb.expires.size() == 0: break
# 随机获取一个带有过期时间的键
key_with_ttl = redisDb.expires.get_random_key()
# 检查键是否过期,如果过期就删除它
if is_expired(key_with_ttl):
delete_key(key_with_ttl)
# 已达到时间上限,停止处理
if reach_time_limit(): return
activeExpireCycle函数的工作模式可以总结如下:
· 函数每次运行时,都从一定数量的数据库中取出一定数量的随机键进行检查,并删除其中的过期键。
· 全局变量current_db会记录当前activeExpireCycle函数检查的进度,并在下一次activeExpireCycle函数调用时,接着上一次的进度进行处理。比如说,如果当前activeExpireCycle函数在遍历10号数据库时返回了,那么下次activeExpireCycle函数执行时,将从11号数据库开始查找并删除过期键。
· 随着activeExpireCycle函数的不断执行,服务器中的所有数据库都会被检查一遍,这时函数将current_db变量重置为0,然后再次开始新一轮的检查工作。
redis内存满了会怎么样
redis使用哪种淘汰策略?
Redis缓存(内存)淘汰策略 |2
规则名称 | 规则说明 |
---|---|
volatile-lru | 使用LRU算法删除一个键(只对设置了生存时间的键) |
allkeys-lru | 使用LRU算法删除一个键 |
volatile-random | 随机删除一个键(只对设置了生存时间的键) |
allkeys-random | 随机删除一个键 |
volatile-ttl | 删除生存时间最近的一个键 |
noeviction | 不删除键,只返回错误 |
Java语言,实现一下LRU缓存?
redis中lru咋实现的?
作者:ce、欢笙
链接:https://www.nowcoder.com/discuss/566337?source_id=discuss_experience_nctrack&channel=-1
定时任务的功能分别如下:通过向主从节点发送info命令获取最新的主从结构;通过发布订阅功能获取其他哨兵节点的信息;通过向其他节点发送ping命令进行心跳检测,判断是否下线。
在从节点中选择新的主节点:选择的原则是,首先过滤掉不健康的从节点;然后选择优先级最高的从节点(由slave-priority指定);如果优先级无法区分,则选择复制偏移量最大的从节点;如果仍无法区分,则选择runid最小的从节点。
应用场景:
1.互联网秒杀
2.抢优惠卷
客户端api:
Jedis
RedisTemplate:Springboot封装好的模板
例子:redis库存 stock-1操作
问题:
多线程并发
解决1:synchronized(this)
适合单体架构(1个tomcat示例运行)
集群:
集群、分布式(多个tomcat部署)
-每个tomcat jvm进程
synchronized 在jvm内部
-整体并发:
2个请求 ngnix 分发到2个tomcat 2个代码段同时操作
-更改端口号 启动程序 可创建多个tomcat实例
-Jmeter 模拟压测的工具
解决2:redis分布式锁-初级理解,问题很多
setnx k v:.setIfAbsent(k,v)
k不存在,设置v;k存在,v不变
完毕后删除k
问题1:
-异常:try finally,finally中删除k
-死锁:try finally中代码,在执行中服务挂了(重启或kill),k未释放,其他请求不到
问题2:死锁:
method:设置超时时间 expire
问题3:
setnx后,未expire成功
method:
原子操作:setIfAbsent()同时设置setnx、expire
高并发场景:
问题4:
-场景:线程A执行任务15',k过期时间10’,->执行过程中,10'后锁已过期;
线程B需8',获取锁,再进行5',锁被A释放;还有C、D...;
=>锁永久失效(k、v是一样的)
-method:
-删除对应k问题:生成唯一标识uuid:原v+“id”,释放前判断是否是自己的v
-程序没执行完,k过期问题:分线程执行定时器timer,k续命,设置过期时间的1/3,如过期时间10',则10/3=3,timer 3'执行一次(注:分布式锁,无论几个tomcat,多线程只有一个timer执行)
redisson框架:redisson.org
-上述思想的实现
-与Jedis类似,redis Java的一个客户端,更适合分布式
问题:redis主从结构
-主从复制时,主挂了,选举,从变成主,新主还没同步k(超高并发下)
m:redlock、zookeeper(推荐,内部也会保证一致性)
-性能优化:分段式
为什么选择redis而不是zookeeper?
-redis性能更高
-zk准确性高
/**
* 不可重入分布式锁的实现 */
@Slf4j
@Component
public class RedisDistributeLock implements DistributeLock {
@Autowired
private RedisTemplate<String, String> redisTemplate;
/**默认key过期时间,单位秒
* 5min*/
private int defaultExpiration = 300;
/** * 非阻塞请求锁 */
@Override
public boolean tryLock(String key, String req) {
return tryLock(key, req, defaultExpiration);
}
/** * 非阻塞请求锁 -默认过期时间*/
@Override
public boolean tryLock(String key, String req, int expiration) {
Boolean state = redisTemplate.opsForValue().setIfAbsent(key, req, expiration, TimeUnit.SECONDS);
if (state != null && state) {
log.info("持有分布式锁{}, req:{}:", key, req);
return true;
}
return false;
}
/*** 阻塞请求锁
* @param timeout 阻塞时长*/
@Override
public boolean tryLock(String key, String req, int expiration, int timeout) {
long start = System.currentTimeMillis();
//毫秒
int period = 10;
for (; ; ) {
boolean lock = tryLock(key, req, expiration);
if (lock) return true;
if (System.currentTimeMillis() - start >= (timeout * 1000)) {
break;
}
try {
TimeUnit.MILLISECONDS.sleep(period);
} catch (InterruptedException e) {
return false;
}
}
return false;
}
/** * 删除分布式锁 */
@Override
public boolean unlock(String key, String req) {
//lua脚本
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end";
RedisScript<Boolean> redisScript = new DefaultRedisScript<>(script, Boolean.class);
Boolean execute = redisTemplate.execute(redisScript, Lists.newArrayList(key), req);
boolean status = execute == null ? false : execute;
if (status) {
log.debug("删除分布式键{}成功:{}", key, req);
}
return status;
}
}
@Scheduled(cron = "0 0/5 * * * ?")
public void LogServiceUserAttrTrackSchedule() {
String key = "ssssss";
String req = UUID.randomUUID().toString();
try {
int expiration = 30 * 60;
boolean holdLock = this.distributeLock.tryLock(key, req, expiration);
if (holdLock) {
// 业务逻辑
// ....
}
} finally {
distributeLock.unlock(key, req);
}
}
2)value要具有唯一性
--用UUID来做,设置随机字符串保证唯一性
--原因:
假如value不是随机字符串,而是一个固定值:
1.客户端1获取锁成功
2.客户端1在某个操作上阻塞了太长时间
3.设置的key过期了,锁自动释放了
4.客户端2获取到了对应同一个资源的锁
5.客户端1从阻塞中恢复过来,因为value值一样,所以执行释放锁操作时就会释放掉客户端2持有的锁,这样就会造成问题
6 释放锁
--解锁时,我们需要判断锁是否是自己的
--lua脚本保持原子性
String script = "if redis.call('get',KEYS[1]) == ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end";
算了...
https://www.cnblogs.com/CareySon/archive/2012/04/25/2470063.html
https://www.zhihu.com/question/50796850
https://zhuanlan.zhihu.com/p/87514615
* 概述
--内存概念不存在时。程序直接访问和操作的都是物理内存。也不存在多进程。
--内存:
为了解决直接操作内存带来的各种问题,引入的地址空间(Address Space),这允许每个进程拥有自己的地址。
还需要硬件上存在两个寄存器,基址寄存器(base register)和界址寄存器(limit register),第一个寄存器保存进程的开始地址,第二个寄存器保存上界,防止内存溢出。
UDP是什么?
UDP怎么实现可靠传输? |2
1)添加seq/ack机制,确保数据发送到对端
2)添加发送和接收缓冲区,主要是用户超时重传。
3)添加超时重传机制。
https://www.jianshu.com/p/6c73a4585eba
tcp参考:
https://blog.csdn.net/qq_38950316/article/details/81087809
https://blog.csdn.net/qzcsu/article/details/72861891
https://www.cnblogs.com/jainszhang/p/10641728.html
TCP是什么?应用场景,udp怎么实现tcp功能
tcp三次握手? |5
tcp四次挥手?以及客户端/服务端分别发送消息后的状态? |4
为什么要三次握手,四次挥手,两次不行么
为什么客户端最后还要等待2MSL tcp的timewait
客户端接收到服务器端的 FIN 报文后进入此状态,此时并不是直接进入 CLOSED 状态,还需要等待一个时间计时器设置的时间 2MSL。这么做有两个理由:
TCP 如何保证可靠传输,讲了一下拥塞控制、滑动窗口/tcp可靠性/为什么是可靠的
1)可靠传输:通过序列号、检验和、确认应答信号、重发控制、连接管理、窗口控制、流量控制、拥塞控制实现可靠性。(重传机制?)
2)TCP 滑动窗口
窗口允许发送方在收到ACK之前连续发送多个分组,窗口的大小就是指无需等待确认应答而可以继续发送数据的最大值。
https://blog.csdn.net/TJtulong/article/details/89858678
TCP协议怎么保证传输可靠性,如果收到了重复数据怎么办?
TCP流量控制?
tcp拥塞控制? |3
tcp拥塞控制怎么实现
TCP用的是ipoc还是什么?
https://blog.csdn.net/nigar_/article/details/104237780
2PC 和 3PC 都是数据库层面的,而 TCC 是业务层面的分布式事务
1 概念
-- 分布式事务顾名思义就是要在分布式系统中实现事务,它其实是由多个本地事务组合而成。
2 常见的分布式事务
-- 2PC、3PC、TCC、本地消息表、消息事务、最大努力通知
3 2PC
-- 2PC(Two-phase commit protocol),中文叫二阶段提交。
-- 二阶段提交是一种强一致性设计,2PC 引入一个事务协调者的角色来协调管理各参与者(也可称之为各本地资源)的提交和回滚,二阶段分别指的是准备(投票)和提交两个阶段。
-- 2PC的问题:同步阻塞协议
1>P失败:分布式事务执行失败
参与者返回失败,那么协调者就会向所有参与者发送回滚事务的请求。
有协调者有超时机制:超时后就会判断事务失败,向所有参与者发送回滚命令。
2>C失败
① 回滚事务操作:不断重试,阻塞P
② 提交事务操作:只能重试
-- 协调者故障分析
协调者是一个单点,存在单点故障问题。
-- 协调者故障,通过选举得到新协调者
每个参与者自身的状态只有自己和协调者知道
数据不一致问题。
-- 总结:
--- 2PC 是一种尽量保证强一致性的分布式事务,因此它是同步阻塞的,而同步阻塞就导致长久的资源锁定问题,总体而言效率低,并且存在单点故障问题,在极端条件下存在数据不一致的风险。
--- 当然具体的实现可以变形,而且 2PC 也有变种,例如 Tree 2PC、Dynamic 2PC。
--- 2PC 适用于数据库层面的分布式事务场景
4 3PC
-- 概念:
3PC 的出现是为了解决 2PC 的一些问题,相比于 2PC 它在参与者中也引入了超时机制,并且新增了一个阶段使得参与者可以利用这一个阶段统一各自的状态。
-- 三个阶段:
准备阶段、预提交阶段和提交阶段,CanCommit、PreCommit 和 DoCommit。
-- 事务失败:
不管哪一个阶段有参与者返回失败都会宣布事务失败,这和 2PC 是一样的(当然到最后的提交阶段和 2PC 一样只要是提交请求就只能不断重试)。
-- 准备阶段:
不会直接执行事务,而是会先去询问此时的参与者是否有条件接这个事务,因此不会一来就干活直接锁资源,使得在某些资源不可用的情况下所有参与者都阻塞着。
-- 预提交阶段:
作用:统一状态。
像一道栅栏,表明在预提交阶段前所有参与者其实还未都回应,在预处理阶段表明所有参与者都已经回应了
但多引入一个阶段也多一个交互,因此性能会差一些,而且绝大部分的情况下资源应该都是可用的,这样等于每次明知可用执行还得询问一次。
-- 参与者超时:
如果是等待提交命令超时,那么参与者就会提交事务了,,如果是等待预提交命令超时,那该干啥就干啥了。
问题:超时机制也会带来数据不一致的问题,比如在等待提交命令时候超时了,参与者默认执行的是提交事务操作,但是有可能执行的是回滚操作,这样一来数据就不一致了。
5 TCC
-- TCC 是业务层面的分布式事务,如发送短信,上传一张图片或者发送一条短信等
-- 概念:TCC 指的是Try - Confirm - Cancel。
Try 指的是预留,即资源的预留和锁定,注意是预留。
Confirm 指的是确认操作,这一步其实就是真正的执行了。
Cancel 指的是撤销操作,可以理解为把预留阶段的动作撤销了。
-- 操作:
比如说一个事务要执行A、B、C三个操作,那么先对三个操作执行预留动作。如果都预留成功了那么就执行确认操作,如果有一个预留失败那就都执行撤销动作。
-- 事务管理者:
TCC模型还有个事务管理者的角色,用来记录TCC全局事务状态并提交或者回滚事务。
-- 注意:
TCC 对业务的侵入较大和业务紧耦合,需要根据特定的场景和业务逻辑来设计相应的操作。
撤销和确认操作的执行可能需要重试,因此还需要保证操作的幂等。
-- TCC可以跨数据库、跨不同的业务系统来实现事务。
6 本地消息表
-- 概念:
利用了 各系统本地的事务来实现分布式事务。
顾名思义就是会有一张存放本地消息的表,一般都是放在数据库中,然后在执行业务的时候 将业务的执行和将消息放入消息表中的操作放在同一个事务中,这样就能保证消息放入本地表中业务肯定是执行成功的。
--实现的是最终一致性,容忍了数据暂时不一致的情况。
1 概念
轻量级的开源的J2EE框架。它是一个容器框架,用来装javabean(java对象),中间层框架(万能胶)
可以起一个连接作用,比如说把Struts和hibernate粘合在一起运用,可以让我们的企业开发更快、更简洁
Spring是一个轻量级的控制反转(IoC)和面向切面(AOP)的容器框架
--从大小与开销两方面而言Spring都是轻量级的。
--通过控制反转(IoC)的技术达到松耦合的目的
--提供了面向切面编程的丰富支持,允许通过分离应用的业务逻辑与系统级服务进行内聚性的
开发
--包含并管理应用对象(Bean)的配置和生命周期,这个意义上是一个容器。
--将简单的组件配置、组合成为复杂的应用,这个意义上是一个框架。
spring 和 springboot的关系
2 Bean的定义方式
4种:xml()、@Bean、@Component、BeanDefinition
① bean在Spring的xml中定义Bean Spring读取类中的构造方法建造的对象
② @Bean在一个方法上 方法中new出来的一个对象
③ @Component注解[还有其他注解]放在类上注入一个Bean
④ 前3都是声明式的方式来注册bean的,而它们的基础都是基于BeanDefinition的方法(编程式)来实现注册Bean的。
--一个Bean的描述、定义
/**
* 通过BeanDefinition 的编程式方式来定义一个Bean
*/
AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext();
AbstractBeanDefinition beanDefinition = BeanDefinitionBuilder.genericBeanDefinition().getBeanDefinition();
// 定义一个bean
beanDefinition.setBeanClass(user.class);
// 添加到spring容器中,注册到ApplicationContext
applicationContext.registerBeanDefinition("user",beanDefinition);
3 依赖注入
--“获得依赖对象的过程被反转了”。控制被反转之后,获得依赖对象的过程由自身管理变为了由IOC容器主动注入。依赖注入是实现IOC的方法,就是由IOC容器在运行期间,动态地将某种依赖关系注入到对象之中。
3)深入理解
Spring 时代我们一般通过 XML 文件来配置 Bean,后来开发人员觉得 XML 文件来配置不太好,于是SpringBoot注解配置就慢慢开始流行起来。
3 ApplicationContext
1)ApplicationContext接口
--获取资源、发布事件、国际化
如ClassPathXmlApplicationContext继承了ApplicationContext接口
public interface ApplicationContext extends EnvironmentCapable, ListableBeanFactory, HierarchicalBeanFactory, MessageSource, ApplicationEventPublisher, ResourcePatternResolver {
...
继承了beanFactory
public interface ListableBeanFactory extends BeanFactory {...
public interface HierarchicalBeanFactory extends BeanFactory {...
--ApplicationContext分类角度:
可刷新、不可刷新
Spring配置的展现形式 xml,注解
2)实现类
AnnotationConfigApplicationContext (注解)
ClassPathXmlApplicationContext(xml)
FileSystemXmlApplicationContext(xml)
--区别:
ClassPathXmlApplicationContext的xml相对的是classPath的路径 如,spring.xml
FileSystemXmlApplicationContext的xml文件相对的是工程的路径 如,/src/main/resources/spring.xml
--注解:一种写法
可刷新、不可刷新
AnnotationConfigApplicationContext (不可刷新)
ClassPathXmlApplicationContext(可刷新 有点类似于热部署)
区别:BeanFactory是个Factory,也就是IOC容器或对象工厂,FactoryBean是个Bean。在Spring中,所有的Bean都是由BeanFactory(也就是IOC容器)来进行管理的。但对FactoryBean而言,这个Bean不是简单的Bean,而是一个能生产或者修饰对象生成的工厂Bean,它的实现与设计模式中的工厂模式和修饰器模式类似
spring-03
--IoC 是设计思想,DI 是具体的实现方式;
IoC 是理论,DI 是实践;
--概念
依赖注入(Dependency Injection,DI)。
依赖 : 指Bean对象的创建依赖于容器 . Bean对象的依赖资源 .
注入 : 指Bean对象所依赖的资源 , 由容器来设置和装配 .
--3种注入方式
① 构造器注入(上面已讲)
② Set方式注入【重点】
③ 扩展方式注入
1)构造器构造
1)类
new User()输出:
User 的无参构造!
2)注册bean
测试:
输出:getBean的时候对象已经创建
2 有参构造:
三种构造:
2)Set方式注入【重点】
1)完整包括:set、get和以下
2)注入
3)扩展方式注入
1 xml配置
1)数据:一个人有两个宠物
2)自动装配
重复装配
输出:
miao~
wang~
2)注解实现自动装配
1 spring的注解
@Autowired和@Qualifier
1)配置
2)使用:可以忽略set方法
坑:
坑:
@Qualifier
3.spring的两个特性IOC和AOP?
6.spring的IOC和Aop介绍一下?
(说了反射、工厂模式和动态代理,之前看过一点源码,说的比较详细,包括每步调了什么方法等)
4.Spring的AOP自调用问题。
7.aop 你怎么使用的aop
3 使用Spring实现Aop
【重点】使用AOP织入,需要导入一个依赖包!
<!-- https://mvnrepository.com/artifact/org.aspectj/aspectjweaver -->
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.4</version>
</dependency>
实现一:第一种方式:通过 Spring API 实现
Aop的重要性 : 很重要 . 一定要理解其中的思路 , 主要是思想的理解这一块 .
Spring的Aop就是将公共的业务 (日志 , 安全等) 和领域业务结合起来 , 当执行领域业务时 , 将会把公共业务加进来 . 实现公共业务的重复利用 . 领域业务更纯粹 , 程序猿专注领域业务 , 其本质还是动态代理 .
1)首先编写我们的业务接口和实现类
// 增删改查接口
public interface UserService {
public void add();
public void delete();
public void update();
public void search();
}
// 实现类
public class UserServiceImpl implements UserService{
@Override
public void add() {
System.out.println("增加用户");
}
@Override
public void delete() {
System.out.println("删除用户");
}
@Override
public void update() {
System.out.println("更新用户");
}
@Override
public void search() {
System.out.println("查询用户");
}
}
2)然后去写我们的增强类 , 我们编写两个 , 一个前置增强 一个后置增强
//前置增强
public class Log implements MethodBeforeAdvice {
//method : 要执行的目标对象的方法
//objects : 被调用的方法的参数
//Object : 目标对象
@Override
public void before(Method method, Object[] objects, Object o) throws Throwable {
System.out.println( o.getClass().getName() + "的" + method.getName() + "方法被执行了");
}
}
//后置增强
public class AfterLog implements AfterReturningAdvice {
//returnValue 返回值
//method被调用的方法
//args 被调用的方法的对象的参数
//target 被调用的目标对象
@Override
public void afterReturning(Object returnValue, Method method, Object[] args, Object target) throws Throwable {
System.out.println("执行了" + target.getClass().getName()
+"的"+method.getName()+"方法,"
+"返回值:"+returnValue);
}
}
3)最后去spring的文件中注册 , 并实现aop切入实现 , 注意导入约束 .
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd">
<!--注册bean-->
<bean id="userService" class="com.kuang.service.UserServiceImpl"/>
<bean id="log" class="com.kuang.log.Log"/>
<bean id="afterLog" class="com.kuang.log.AfterLog"/>
<!--aop的配置-->
<aop:config>
<!--切入点 expression:表达式匹配要执行的方法-->
<aop:pointcut id="pointcut" expression="execution(* com.kuang.service.UserServiceImpl.*(..))"/>
<!--执行环绕; advice-ref执行方法 . pointcut-ref切入点-->
<aop:advisor advice-ref="log" pointcut-ref="pointcut"/>
<aop:advisor advice-ref="afterLog" pointcut-ref="pointcut"/>
</aop:config>
</beans>
4)测试类
public class MyTest {
@Test
public void test(){
ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");
UserService userService = (UserService) context.getBean("userService");
userService.search();
}
}
实现二:自定义类来实现Aop
--目标业务类不变依旧是userServiceImpl
1)第一步 : 写我们自己的一个切入类
public class DiyPointcut {
public void before(){
System.out.println("---------方法执行前---------");
}
public void after(){
System.out.println("---------方法执行后---------");
}
}
2)去spring中配置
<!--第二种方式自定义实现-->
<!--注册bean-->
<bean id="diy" class="com.kuang.config.DiyPointcut"/>
<!--aop的配置-->
<aop:config>
<!--第二种方式:使用AOP的标签实现-->
<aop:aspect ref="diy">
<aop:pointcut id="diyPonitcut" expression="execution(* com.kuang.service.UserServiceImpl.*(..))"/>
<aop:before pointcut-ref="diyPonitcut" method="before"/>
<aop:after pointcut-ref="diyPonitcut" method="after"/>
</aop:aspect>
</aop:config>
3)测试
public class MyTest {
@Test
public void test(){
ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");
UserService userService = (UserService) context.getBean("userService");
userService.add();
}
}
实现三:使用注解实现
1)第一步:编写一个注解实现的增强类
@Aspect //标注这个类是一个切面
public class AnnotationPointcut {
@Before("execution(* com.kuang.service.UserServiceImpl.*(..))")
public void before(){
System.out.println("---------方法执行前---------");
}
@After("execution(* com.kuang.service.UserServiceImpl.*(..))")
public void after(){
System.out.println("---------方法执行后---------");
}
// 在环绕增强中,我们可以给定一个参数,代表我们要获取处理切入的点
@Around("execution(* com.kuang.service.UserServiceImpl.*(..))")
public void around(ProceedingJoinPoint jp) throws Throwable {
System.out.println("环绕前");
System.out.println("签名:"+jp.getSignature());
//执行目标方法proceed
Object proceed = jp.proceed();
System.out.println("环绕后");
System.out.println(proceed);
}
}
2)第二步:在Spring配置文件中,注册bean,并增加支持注解的配置
<!--第三种方式:注解实现-->
<bean id="annotationPointcut" class="com.kuang.config.AnnotationPointcut"/>
<aop:aspectj-autoproxy/>
aop:aspectj-autoproxy:说明
--通过aop命名空间的声明自动为spring容器中那些配置@aspectJ切面的bean创建代理,织入切面。当然,spring 在内部依旧采用AnnotationAwareAspectJAutoProxyCreator进行自动代理的创建工作,但具体实现的细节已经被隐藏起来了
--有一个proxy-target-class属性,默认为false,表示使用jdk动态代理织入增强,当配为时,表示使用CGLib动态代理技术织入增强。不过即使proxy-target-class设置为false,如果目标类没有声明接口,则spring将自动使用CGLib动态代理。
3)测试
public class MyTest {
@Test
public void test(){
ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");
UserService userService = (UserService) context.getBean("userService");
userService.add();
}
}
tips:
代理模式 基于接口 JDK动态(默认) 基于类 cglib
参数设置false为JDK;默认false ;几乎不用,设置为true 结果无区别
2 实体-表
@Data
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
@KeySequence("DM_OPERATION_LOG_SEQUENCE")
public class DmOperationLog implements Serializable {
@TableId
private Long id;
@TableField(fill = FieldFill.INSERT)
private String userName;
@TableField(fill = FieldFill.INSERT)
private String userIp;
@TableField(fill = FieldFill.INSERT)
private String accessUrl;
@TableField(fill = FieldFill.INSERT)
private String operationDesc;
@TableField(fill = FieldFill.INSERT)
private String statusCode;
@TableField(fill = FieldFill.INSERT)
private String operationStatus;
@TableField(fill = FieldFill.INSERT)
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private Date operationTime;
}
3 aspect
1)OperationLog
//HISTORY_BATCH("hsitory_batch","历史调用-批量");
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface OperationLog {
/**
* 操作类型
* @return
*/
OperationTypeEnum type() default OperationTypeEnum.HISTORY_SINGLE;
}
2)OperationAopLog
主要将操作日志存入表
/**
* 操作日志切面
*/
@Aspect
@Component
@Slf4j
public class OperationAopLog {
@Resource
IDmOperationLogService operationLogService;
@Pointcut(value = "@annotation(com.rong360.tianji.tob.suzhou.data.admin.aspect.OperationLog)")
public void log() {
}
@Around("log()&& @annotation(operationLog)")
public Object aroundLog(ProceedingJoinPoint point,OperationLog operationLog) throws Throwable {
DmOperationLog dmOperationLog = new DmOperationLog();
//设置用户名和操作描述
String userName = getUserName(operationLog, point);
dmOperationLog.setUserName(userName);
dmOperationLog.setOperationDesc(operationLog.type().getZhName());
//设置用户IP和访问URL
setIPAndURL(dmOperationLog);
Object result = null;
try {
//方法执行
result = point.proceed();
//结果判断
if(result instanceof Result){
String statusCode = ((Result) result).getCode().toString();
dmOperationLog.setStatusCode(statusCode);
String operationStatus = "200".equals(statusCode)? OperationStatusEnum.SUCCESS.getZhName() :OperationStatusEnum.FAIL.getZhName();
dmOperationLog.setOperationStatus(operationStatus);
}else {
dmOperationLog.setStatusCode("200");
dmOperationLog.setOperationStatus(OperationStatusEnum.SUCCESS.getZhName());
}
saveOperationLog(dmOperationLog);
} catch (Exception e){
dmOperationLog.setStatusCode("400");
dmOperationLog.setStatusCode(OperationStatusEnum.FAIL.getZhName());
saveOperationLog(dmOperationLog);
throw e;
}
return result;
}
/**
* 获取用户名
* @param operationLog
* @param point
* @return
*/
private String getUserName(OperationLog operationLog, ProceedingJoinPoint point) {
try {
//登录操作从接口参数获取用户名,其他操作从session获取用户名
if (operationLog.type() == OperationTypeEnum.LOGIN) {
Object[] args = point.getArgs();
LoginVO loginVO = (LoginVO) args[0];
return loginVO.getName();
} else {
Session session = SecurityUtils.getSubject().getSession();
return session.getAttribute("user_name").toString();
}
} catch (Exception e) {
log.error("get user name error:{}", e);
return "";
}
}
/**
* 设置用户ip和访问url
* @param dmOperationLog
*/
private void setIPAndURL(DmOperationLog dmOperationLog){
HttpServletRequest request = null;
try {
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
request = attributes.getRequest();
dmOperationLog.setUserIp(IPUtils.getIP(request));
dmOperationLog.setAccessUrl(request.getRequestURI());
}catch (Exception e){
log.error("set IP and URL error:{}", e);
}
}
/**
* 操作记录入库
*
* @param dmOperationLog
*/
private void saveOperationLog(DmOperationLog dmOperationLog) {
try {
dmOperationLog.setOperationTime(new Date());
operationLogService.save(dmOperationLog);
}catch (Exception e){
log.error("save operation OperationLogController error:{}", e);
}
}
}
3 使用
@OperationLog(type = OperationTypeEnum.HISTORY_BATCH)
@PostMapping("savetask")
@RequiresPermissions("userquery:multi")
public Result QueryTask(@RequestBody QueryTaskInputVO queryTaskInputVO) {
String message = queryTaskService.checkTaskInput(queryTaskInputVO);
if (!StrUtil.isEmpty(message)) {
return Result.fail(400, message);
}
Boolean state = queryTaskService.insertTask(queryTaskInputVO);
if (state) {
return Result.success();
}
return Result.fail();
}
4 对表的操作:OperationLogController
@RestController
@RequestMapping(value = "/logManage")
@Slf4j
public class OperationLogController {
@Autowired
IOperationLogService operationLogService;
@Value("classpath:json/table-header-log.json")
private org.springframework.core.io.Resource logTableHeader;
/**
* 获取下拉选项
* @return
*/
@RequestMapping(value = "/search")
@RequiresPermissions("log:view")
public Result getSelectData() {
try {
Map<String, Object> map = operationLogService.getSelectData();
return Result.success(map);
} catch (Exception e) {
log.error("获取下拉框失败:{}", e);
return Result.fail(400, "内部异常");
}
}
/**
* 获取操作日志列表
*
* @param operationLogVO
* @return
*/
@RequestMapping(value = "/list")
@RequiresPermissions("log:view")
public Result list(@RequestBody OperationLogVO operationLogVO){
try {
// 按日期查询的时候 2020-01-01 - 2020-01-01 查询的实际截至时间是 2020-01-02 00:00:00
if (operationLogVO.getEndTime()!=null){
Date voEndTime = DateUtil.parse(operationLogVO.getEndTime(), "yyyy-MM-dd");
DateTime relEndTime = DateUtil.offset(voEndTime, DateField.DAY_OF_MONTH, 1);
operationLogVO.setEndTime(DateUtil.formatDate(relEndTime));
}
Map<String, Object> map = operationLogService.getList(operationLogVO);
map.put("columns", Utils.getJsonArray(logTableHeader));
return Result.success(map);
} catch (Exception e) {
log.error("获取配置列表失败:{}", e);
return Result.fail(400, "内部异常");
}
}
/**
* 操作日志列表导出
*
* @return
*/
@GetMapping(value = "/download")
@RequiresPermissions("log:view")
public void export(HttpServletResponse response,
@RequestParam(value = "log_desc", required = false) String operationDesc,
@RequestParam(value = "log_user", required = false) String userName,
@RequestParam(value = "end_time", required = false) String endTime,
@RequestParam(value = "start_time", required = false) String startTime,
@RequestParam(value = "log_ip", required = false) String userIp,
@RequestParam(value = "log_url", required = false) String accessUrl,
@RequestParam(value = "log_status", required = false) String operationStatus,
@RequestParam(value = "log_code", required = false) String statusCode) {
try {
OperationLogVO operationLogVO = new OperationLogVO();
if (StringUtils.isNotBlank(userName)) {
operationLogVO.setUserName(userName);
}
if (StringUtils.isNotBlank(operationDesc)) {
operationLogVO.setOperationDesc(operationDesc);
}
if (StringUtils.isNotBlank(userIp)) {
operationLogVO.setUserIp(userIp);
}
if(StringUtils.isNotBlank(accessUrl)){
operationLogVO.setAccessUrl(accessUrl);
}
if(StringUtils.isNotBlank(statusCode)){
operationLogVO.setStatusCode(statusCode);
}
if(StringUtils.isNotBlank(operationStatus)){
operationLogVO.setOperationStatus(operationStatus);
}
if(StringUtils.isNotBlank(startTime)){
operationLogVO.setStartTime(startTime);
}
if(StringUtils.isNotBlank(endTime)){
Date voEndTime = DateUtil.parse(endTime);
Date relEndTime = DateUtil.offset(voEndTime, DateField.DAY_OF_MONTH, 1);
operationLogVO.setEndTime(DateUtil.formatDate(relEndTime));
}
operationLogService.exportList(response,operationLogVO);
} catch (Exception e) {
log.error("获取配置列表导出失败:{}", e);
//return Result.fail(400, "内部异常");
}
}
}
1、解析类得到BeanDefinition
2、如果有多个构造方法,则要推断构造方法
3、确定好构造方法后,进行实例化得到一个对象
4、对对象中的加了@Autowired注解的属性进行属性填充
5、回调Aware方法,比如BeanNameAware,BeanFactoryAware
6、调用BeanPostProcessor的初始化前的方法
7、调用初始化方法
8、调用BeanPostProcessor的初始化后的方法,在这里会进行AOP
9、如果当前创建的bean是单例的则会把bean放入单例池
10、使用bean
11、Spring容器关闭时调用DisposableBean中destory()方法
1)什么是循环依赖
@Component
public class A {
// A中注入了B
@Autowired
private B b;
}
@Component
public class B {
// B中也注入了A
@Autowired
private A a;
}
https://blog.csdn.net/qq_34125999/article/details/114858004
在使用Spring框架时,可以有两种使用事务的方式,一种是编程式的,一种是申明式的,
--@Transactional注解就是申明式的。
首先,事务这个概念是数据库层面的,Spring只是基于数据库中的事务进行了扩展,以及提供了一些能让程序员更加方便操作事务的方式。
比如我们可以通过在某个方法上增加@Transactional注解,就可以开启事务,这个方法中所有的sql都会在一个事务中执行,统一成功或失败。
在一个方法上加了@Transactional注解后,Spring会基于这个类生成一个代理对象,会将这个代理对象作为bean,当在使用这个代理对象的方法时,如果这个方法上存在@Transactional注解,那么代理逻辑会先把事务的自动提交设置为false,然后再去执行原本的业务逻辑方法,如果执行业务逻辑方法没有出现异常,那么代理逻辑中就会将事务进行提交,如果执行业务逻辑方法出现了异常,那么则会将事务进行回滚。
当然,针对哪些异常回滚事务是可以配置的,可以利用@Transactional注解中的rollbackFor属性进行配置,默认情况下会对RuntimeException和Error进行回滚。
spring事务隔离级别就是数据库的隔离级别:外加一个默认级别
read uncommitted(未提交读)
read committed(提交读、不可重复读)
repeatable read(可重复读)
serializable(可串行化)
数据库的配置隔离级别是Read Commited,而Spring配置的隔离级别是Repeatable Read,请问这时隔离级别是以哪一个为准?
以Spring配置的为准,如果spring设置的隔离级别数据库不支持,效果取决于数据库
spring事务的原理是AOP,进行了切面增强,那么失效的根本原因是这个AOP不起作用了!常见情况有如下几种
1、发生自调用,类里面使用this调用本类的方法(this通常省略),此时这个this对象不是代理类,而是UserService对象本身!
解决方法很简单,让那个this变成UserService的代理类即可!
2、方法不是public的
@Transactional 只能用于 public 的方法上,否则事务不会失效,如果要用在非 public 方法上,可以开启 AspectJ 代理模式。
3、数据库不支持事务
4、没有被spring管理
5、异常被吃掉,事务不会回滚(或者抛出的异常没有被定义,默认为RuntimeException)
@Transactional 的工作机制是基于 AOP 实现的,AOP 又是使用动态代理实现的。如果目标对象实现了接口,默认情况下会采用 JDK 的动态代理,如果目标对象没有实现了接口,会使用 CGLIB 动态代理。**
多提一嘴:createAopProxy() 方法 决定了是使用 JDK 还是 Cglib 来做动态代理
使用spring + springmvc使用,如果需要引入mybatis等框架,需要到xml中定义mybatis需要的bean, starter就是定义一个starter的jar包,写一个@Configuration配置类、将这些bean定义在里面,然后在starter包的META-INF/spring.factories中写入该配置类,springboot会按照约定来加载该配置类开发人员只需要将相应的starter包依赖进应用,进行相应的属性配置(使用默认配置时,不需要配置),就可以直接进行代码开发,使用对应的功能了,比如mybatis-spring-boot--starter,springboot-starter-redis
8.如果Controller层想返回的数据是JSON格式的,怎么办。
RestController
https://zhuanlan.zhihu.com/p/104941876
https://www.cnblogs.com/ysocean/p/9227233.html
mysql运维
mysql 5.6 => 5.7
redis-占数师
redis-server --version
Redis server v=4.0.10 sha=00000000:0 malloc=jemalloc-4.0.3 bits=64 build=9e1e501b91e06bf9
redis-oa
2.6 其他运维告知 3.0.6
kafka版本
offset保存位置不同
zookeeper环境:3.4.14-已跟线上版本一致
kafka单机环境(2.11-2.0.0版本)
Kafka-外数(2.2.9)
org.springframework.kafka
spring-kafka
背景:
针对app、微信等多种渠道的用户行为数据进行采集;将各类行为数据进行整合,与公司内线下数据、公司外数据结合;开展行为数据分析,提供相应数据分析功能和工具;提供相关业务功能和可视化展示页面。
工程中用到的技术
1)需求
1> 元数据使用流程:
2> 元数据上下游关系
2)技术方案
3)优化
使用zookeeper
留存分析需求
1)日留存中间表schema设计
a、log_dt,skynet_user_id 作为主键
b、表命名规则?krs.tmp_用户_时间戳?
前端:Vue + highChart
后端: Spring boot + MyBatis-Plus+ Quartz+ QLExpress + Shiro
数据库:Oracle
缓存+队列:Redis
监控+日志收集:Open-Falcon+Kibana
使用
// key = module_uniqName_subid_timestamp
$rowkey = $data['model']."_".$data['md5'];
//hbase2://co_protocol/protocol_timestamp_a4448a7f26968c0f66476c7527c2b946_1609810061453
$rowkey = $data['model']."_".$data['md5']."_".intval(microtime(true)*1000);
$column = array();
$column['d:'.$data['model']] = $data['content'];
$column['d:filetype'] = $data['filetype'];
rowkey(行主键) 列名 d:filetype 时间戳