Java架构师学习之路之无锁队列ConcurrentLinkedQueue分析
本文章主要尝试分析一下多线程并发插入ConcurrentLinkedQueue时的流程。
JDK版本:1.8
接下来开始查看源码:
- 首先创建一个ConcurrentLinkedQueue对象,并调用add方法:
final ConcurrentLinkedQueue<String> strings = new ConcurrentLinkedQueue<>();
strings.add("123");
- 接着进入add方法查看:
public boolean add(E e) {
return offer(e);
}
- 可以发现调用了offer方法,那么我们查看一下offer方法:
public boolean offer(E e) {
checkNotNull(e);
final Node<E> newNode = new Node<E>(e);
for (Node<E> t = tail, p = t;;) {
Node<E> q = p.next;
if (q == null) {
// p is last node
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".
if (p != t) // hop two nodes at a time
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.
p = (p != t && t != (t = tail)) ? t : q;
}
}
可以看到,add方法的核心部分就在这里了。
那么接下来我们用图文结合的方式,并根据注释来分析一下,如何做到无锁并发插入的。
通过阅读注释,我们可以看到,else if这个分支是用于移除元素的。
所以将代码精简一下得到如下代码:
public boolean offer(E e) {
final Node<E> newNode = new Node<E>(e);
for (Node<E> t = tail, p = t;;) {
Node<E> q = p.next;
if (q == null) {
if (p.casNext(null, newNode)) {
if (p != t)
casTail(t, newNode);
return true;
}
}
else
p = (p != t && t != (t = tail)) ? t : q;
}
}
看起来就相对容易了很多。接下来进入到图文结合的流程:
- 首先是没有任何一个元素的队列,我们要并发插入一个Node A节点和Node B节点,此时tail指针指向null:

- 接下来进入到循环中,此时想要插入Node A和Node B的线程中都会初始化 t指针和p指针,都指向null:

- 接下来创建了q指针,但是显然q = p.next = null。此时两个线程的q都是null,判断为true,所以此时会进行
p.casNext(null, newNode)的操作。可以看到casNext方法如下:
boolean casNext(Node<E> cmp, Node<E> val) {
return UNSAFE.compareAndSwapObject(this, nextOffset, cmp, val);
}
这个方法是原子操作,因此只有一个线程能替换成功,其他的都将失败。因此我们假设Node A的替换成功,Node B替换失败:

此时Node B失败了,因此进入下一次循环。同时咱们看看Node A的操作:
if (p != t)
casTail(t, newNode); // Failure is OK.
return true;
我们可以通过图发现,指针p 和 指针t 都没有发生改变,因此 p != t为false,最后返回true,结束Node A的插入。
记住此时的状态:
tail指向第一个节点,第一个节点的next指向Node A。
第二次并发插入:
4. 如果此时Node B和Node C进行并发插入,初始状态为:

5. 接下来创建指针q;

由于 q = p.next,因此q指向Node A。
- 由于q指向的地址不为null,所以进入else代码:
p = (p != t && t != (t = tail)) ? t : q;
注意这段代码,咱们一步步分析一下:
首先整体上看,其实就是对指针p赋值,要么是指针t,要么是指针q。此时指针t指向首节点,指针q指向Node A。
当p != t && t != (t = tail)时,赋值为指针t,否则赋值为指针q。
此时Node B线程和Node C线程同时在各自的线程栈中进行判断:
p != t一定为false(看图),所以赋值为指针q,即Node A节点。然后各自都进入下一次循环。
- 第二次循环中,
指针t = tail和指针p 指向 Node A,并且重新创造了指针q 指向 null。

- 接下来进入了
q==null的分支,又进行casNext的替换,于时,Node B 和 Node C 只有一者能替换成功,失败者将进入下次循环。假设此处Node C添加成功:

- 接下来执行
if (p != t)
casTail(t, newNode);
return true;
显然,此时 p != t 为 ture,因此执行tail指针的CAS替换:

最终结束插入。
为什么不是每次线程结束都去修改一次tail指针呢?
因为并发插入的场合下,很有可能某个线程修改tail指针的时候,其他线程又插入了新的节点,该tail指针无法指向最新的节点。
而设计成JDK1.8的这种模式,则其他线程必须要通过p = (p != t && t != (t = tail)) ? t : q;将 指针p 更新到最新的 tail 指针,才能进入 q == null的分支。而指针p 的更新,需要某一个线程进行CAS替换操作。
欢迎大家一起讨论学习,若有不足之处,请各位大佬予以指正!

本文详细分析了Java 1.8中ConcurrentLinkedQueue的无锁插入机制,通过源码解析展示了在多线程环境下如何通过CAS操作实现并发安全的节点插入。在循环中,利用p和t指针的更新以及casNext和casTail方法,确保了队列的线程安全性和高效性。文章以图文并茂的方式解释了并发插入的流程,揭示了无锁数据结构在并发控制上的巧妙设计。
1922





