结尾
这不止是一份面试清单,更是一种”被期望的责任“,因为有无数个待面试者,希望从这篇文章中,找出通往期望公司的”钥匙“,所以上面每道选题都是结合我自身的经验于千万个面试题中经过艰辛的两周,一个题一个题筛选出来再次对好答案和格式做出来的,面试的答案也是再三斟酌,深怕误人子弟是小,影响他人仕途才是大过,也希望您能把这篇文章分享给更多的朋友,让他帮助更多的人,帮助他人,快乐自己,最后,感谢您的阅读。
由于细节内容实在太多啦,在这里我花了两周的时间把这些答案整理成一份文档了,在这里只把部分知识点截图出来粗略的介绍,每个小节点里面都有更细化的内容!
-
一种是阻塞算法,也就是用锁去控制队列的入队和出队,可以是两个锁也可以是一个锁
-
另外一种就是非阻塞算法,采用循环CAS的方式去实现,其实也就是乐观锁
而ConcurrentLinkedQueue的实现方式就是非阻塞算法
ConcurrentLinkedQueue是一个基于链接节点的无界线程安全队列,采用先进先出(FIFO)的规则对节点进行排序,传统的队列都是这样,新添加的元素在队列的尾部,获取一个元素时,返回队列头部的元素,采用CAS算法去实现,也称为wait-free算法
下面就来看看这个类的结构
可以看到其类架构
ConcurrentLinkedQueue继承了AbstractQueue,AbstractQueue继承了AbstractCollection
从图中可以看到这个AbstractCollection实现了Collection接口,前面学习List接口,也有接触这个AbstractCollection,所以可以看到这个队列的实现跟List和Set的实现类可能有点共同之处。
下面就来研究一下这个ConcurrentLinkedQueue是怎样架构的
拥有的变量属性
下面就来看看这几个变量是干什么的
-
serialVersionUID:序列化ID
-
head:头节点
-
tail:尾节点
-
headOffset:头节点偏移量
-
tailOffset:尾节点偏移量
-
UNSAFE:还不清除这个具体干嘛的,在ConcurrentHashMap看过用它来替换元素
Node类
head和tail属性都是其Node内部类,所以先来看这个Node类
其有两个属性
-
item:存储的值
-
next:下一个Node地址
再看里面的一段静态代码,因为实例前会执行静态代码块,所以先看这个
可以看到,这里使用了静态变量去获取item和next字段相对Java对象的“起始地址”的偏移量
所以,这个类一被实例化,就会去获取item和next字段相对实例对象的偏移量了
接下来我们来看里面的方法
可以看到,对于item和next的赋值,是用CAS来实现的,所以这方面保证了并发的安全
构造方法
下面只讲无参构造方法
从无参构造方法可以看到,一开始,head和tail结点都是同一个引用,都是一个空item的Node
入队操作
入队其实就是将入队节点添加到队列的尾部,而入队的操作主要做两件事情
-
将入队节点设置成当前队列尾节点的下一个节点
-
更新tail节点
-
如果tail节点的next节点不为空,则将入队节点设置为tail节点
-
如果tail节点的next节点为空,则将入队节点设置成tail的next节点
-
所以,tail节点不一定是尾节点!头节点只起到一个哨兵作用!
大概流程
一开始,head、tail节点情况
添加第一个节点,由于tail节点下一个节点为null,所以让tail节点指向新入队的节点
添加第二个节点,由于tail节点的next有值了,所以第二个新添加进来的节点成为tail节点
下面的情况就以此类推不再示范了,那为什么要采用这种方式去添加节点呢?而不是直接默认新添加的节点为tail节点呢?
我自己感觉,这样可以减少tail节点变换的次数(只有尾节点和tail节点距离为1时去更新tail),这里的变换是指tail节点地址变换,因为每次去添加一个节点都要去对尾节点更新的,假如让新添加的节点为tail节点的话,就代表每一次添加,tail节点都会变成另外一个,容易产生并发问题,两个线程去同时获取tail节点,而且都为同一个,那就会产生覆盖问题了,后添加的节点会覆盖先添加的节点(虽然说可以利用CAS来避免,但是CAS太多次会影响效率,CAS次数也是要减少的),假如我们通过让tail节点的next节点去控制尾节点,当两个线程同时获取到tail节点也不一定会产生并发问题,假如现在两个节点同时获取了tail节点,并且该tail节点的next为null,那么并不会去修改tail节点,对于另外获取了同一个tail节点的线程是没有影响的,当然,假如同时判断了tail节点的next为null,同样也会产生并发问题
下面来看看ConcurrencyLinkedQueue是怎样使用CAS来入队的,入队的具体实现逻辑在offer方法里面
首先看这个方法给的注释
由于队列是无界的,这个方法永远不会返回false
从这句话大概可以猜测出,返回值为布尔类型代表插入是否成功,并且强调了不会返回false,因为队列是无界的!同时也可以看出这个队列的底层实现是使用链表
public boolean offer(E e) {
//这个方法只是检验传来的值是不是Null
checkNotNull(e);
//去创建一个新的入队节点
final Node newNode = new Node(e);
//死循环遍历·链表得到尾节点,注意不是tail节点
//是从tail节点出发去遍历找到尾节点
for (Node t = tail, p = t;😉 {
//获取当前tail节点的下一个节点
Node q = p.next;
//如果tail节点下一个为Null就代表此时tail是尾节点了
//注意,此时是tail节点第二中情况,tail.next为空
//所以tail节点不变,让新节点直接为tail.next
if (q == null) {
// p is last node
//调用节点本身的CAS方法去进行替换节点
if (p.casNext(null, newNode)) {
// Successful CAS is the linearization point
// for e to become an element of this queue,
// and for newNode to become “live”.
// CAS换tail.next节点成功后
// tail节点进行cas更新,让新插入的节点为tail节点
if (p != t) // hop two nodes at a time
// 并且注意,此时是允许CAS失败的!!!
// 所以会引出第一种情况,tail.next不为空!
casTail(t, newNode); // Failure is OK.
return true;
}
// Lost CAS race to another thread; re-read next
}
//出现下面这种情况,其实就是有一个线程出队的时候产生的
//要下面认识了出队操作才能说明白
else if (p == q)
// We have fallen off list. If tail is unchanged, it
// will also be off-list, in which case we need to
// jump to head, from which all live nodes are always
// reachable. Else the new tail is a better bet.
p = (t != (t = tail)) ? t : head;
else
// Check for tail updates after two hops.
// 由于可能之前CAS更新tail失败,即有人变过tail
// 所以tail.next != null了
// 这种情况就继续遍历下去找到null为即可
p = (p != t && t != (t = tail)) ? t : q;
}
}
看完源码,可以总结其入队流程是怎样的了
对于插入操作的并发,其要对两种情况进行预防,即另外其他线程执行插入与弹出动作
-
首先判断插入的值是不是空
-
如果为空,抛出空指针异常
-
不为空,执行下面的操作
-
通过死循环和tail节点去找到真正的尾节点
-
如果tail节点下一个节点为null,代表已经找到尾节点
-
使用CAS进行改变当前tail.next为新节点
-
如果CAS修改tail.next为新节点成功,再尝试CAS修改tail节点为新节点(允许CAS修改tail节点为新节点失败)
-
如果失败,代表有其他线程对tail.next进行了修改(入队和出队都有可能影响tail.next),则放弃进行CAS修改tail为新节点,并且进入下一轮循环
-
如果此时,tail = tail.next则证明出现了弹出情况(出现这种情况是因为一开始的head和tail是同一个引用,而且出队操作断开与队列的连接采用自引用的方式,一旦出现node == node.next情况,肯定是这个结点被弹出了,所以要重新获取新的tail去进行)
-
所以针对这种情况,就要从head进行遍历了,不可以从tail去遍历找到尾节点了,这是因为所有的节点都能从head进行遍历得到
-
但在这里并不是都会从head进行遍历,其还会去判断此时有没有别的线程进行插入
-
如果tail节点被改过了,证明此时有人插入并且更新了tail节点,下次循环仍然可以从tail节点出发
-
如果tail节点没被动过,证明此时没有人插入。那么就从head开始吧
-
区区一行代码竟如此复杂。。。
-
如果此时,tail.next不为空,那就需要下次循环继续遍历去找到尾节点,这也是为什么在进行新节点替换时是允许失败的!因为会回旋下次进行添加,也就是乐观锁的实现
-
注意,在这里也会去检查,在这个过程中tail有无被改动过
-
如果tail被改动过,就从新的tail出发(有线程进行入队或出队操作)
-
如果tail没被改动过,就从tail.next出发
所以,入队的线程安全保证主要是靠乐观锁与tail节点是否变动来实现
注意:
-
CAS允许失败,采用乐观锁来保证线程安全
-
从tail节点去找尾节点时,如果此时tail节点不是尾节点,都会去判断tail节点是否变动过,只要变动过就会重新获取tail节点去开始下一轮寻找尾节点(因为变动过就代表有线程进行改变),如果没变过,就从tail.next开始
-
特殊情况:进入入队方法,但tail被弹出,导致tail == tail.next,此时也会去判断有无线程把tail给改了,如果改了,就可以重新获取tail开始,如果没改,就要从head开始,因为只有从head开始,才能经过所有正常节点来保证线程安全,并且此时tail被弹出是不可以用的且没被其余线程更新。
《MySql面试专题》
《MySql性能优化的21个最佳实践》
《MySQL高级知识笔记》
文中展示的资料包括:**《MySql思维导图》《MySql核心笔记》《MySql调优笔记》《MySql面试专题》《MySql性能优化的21个最佳实践》《MySq高级知识笔记》**如下图
关注我,点赞本文给更多有需要的人
JfC6YNkv-1715297823951)]
文中展示的资料包括:**《MySql思维导图》《MySql核心笔记》《MySql调优笔记》《MySql面试专题》《MySql性能优化的21个最佳实践》《MySq高级知识笔记》**如下图
[外链图片转存中…(img-oZKXuNvo-1715297823952)]
关注我,点赞本文给更多有需要的人