ConcurrentLinkedQueue 是一个由链表结构组成的无界非阻塞队列,是 JDK 中唯一一个并发安全的非阻塞队列,使用无锁算法来保证线程安全。为了减少 CAS 操作造成的资源争夺损耗,其链表结构被设计为“松弛”的(Slack)。本文对 ConcurrentLinkedQueue 的入队和出队过程进行图解,直观展示其内部结构。
本文基于 jdk1.8.0_91
1. 继承体系
java.util.concurrent.ConcurrentLinkedQueue
public class ConcurrentLinkedQueue<E> extends AbstractQueue<E>
implements Queue<E>, java.io.Serializable
2. 数据结构
ConcurrentLinkedQueue 的数据结构为链表。
2.1 链表节点
需要注意的是,item 为空表示无效节点,非空表示有效节点。
无效节点是需要从链表中清理掉的节点,ConcurrentLinkedQueue 队列中为什么要存储无效节点呢,继续往下看。
java.util.concurrent.ConcurrentLinkedQueue.Node
private static class Node<E> {
volatile E item; // 节点的数据
volatile Node<E> next; // 下一个节点
/**
* Constructs a new node. Uses relaxed write because item can
* only be seen after publication via casNext.
*/
Node(E item) {
UNSAFE.putObject(this, itemOffset, item);
}
boolean casItem(E cmp, E val) {
return UNSAFE.compareAndSwapObject(this, itemOffset, cmp, val);
}
// 相比 putObjectVolatile(),putOrderedObject() 不保证内存可见性,但是性能较高
void lazySetNext(Node<E> val) {
UNSAFE.putOrderedObject(this, nextOffset, val);
}
boolean casNext(Node<E> cmp, Node<E> val) {
return UNSAFE.compareAndSwapObject(this, nextOffset, cmp, val);
}
}
2.2 head 和 tail 节点
队列中定义了 head 和 tail 节点。
由于采用了非阻塞算法(non-blocking algorithms),head 和 tail 节点并不严格指向链表的头尾节点,也就是每次入队出队操作并不会及时更新 head 和 tail 节点。
通过规定“不变式”和“可变式”来维护非阻塞算法的正确性。
不变式:并发对象需要一直保持的特性。
不变式是并发对象的各个方法之间必须遵守的“契约”,每个方法在调用前和调用后都必须保持不变式。
采用不变式,就可以隔离的分析每个方法,而不用考虑它们之间所有可能的交互。
基本不变式
在执行方法之前和之后,队列必须要保持的不变式(The fundamental invariants):
- 当入队插入新节点之后,队列中有一个 next 域为 null 的节点(真正的尾节点)。
- 从 head 开始遍历队列,可以访问所有 item 域不为 null 的节点(有效节点)。
head 的不变式和可变式
/**
* A node from which the first live (non-deleted) node (if any)
* can be reached in O(1) time.
* Invariants:
* - all live nodes are reachable from head via succ()
* - head != null
* - (tmp = head).next != tmp || tmp != head
* Non-invariants:
* - head.item may or may not be null.
* - it is permitted for tail to lag behind head, that is, for tail
* to not be reachable from head!
*/
private transient volatile Node<E> head;
head 的不变式:
- 所有的有效节点,都能从 head 通过调用 succ() 方法遍历可达。
- head 不能为 null。
- head 节点的 next 域不能引用到自身。
head 的可变式:
- head 节点的 item 域可能为 null,也可能不为 null。
- 允许 tail 滞后(lag behind)于 head,也就是说:从 head 开始遍历队列,不一定能到达 tail。
tail 的不变式和可变式
/**
* A node from which the last node on list (that is, the unique
* node with node.next == null) can be reached in O(1) time.
* Invariants:
* - the last node is always reachable from tail via succ()
* - tail != null
* Non-invariants:
* - tail.item may or may not be null.
* - it is permitted for tail to lag behind head, that is, for tail
* to not be reachable from head!
* - tail.next may or may not be self-pointing to tail.
*/
private transient volatile Node<E> tail;
tail 的不变式:
- 通过 tail 调用 succ() 方法,最后节点总是可达的。
- tail 不能为 null。
tail 的可变式:
- tail 节点的 item 域可能为 null,也可能不为 null。
- 允许 tail 滞后于 head,也就是说:从 head 开始遍历队列,不一定能到达 tail。
- tail 节点的 next 域可以引用到自身。
3. 构造函数
默认创建空节点,head 和 tail 都指向该节点。
/**
* Creates a {@code ConcurrentLinkedQueue} that is initially empty.
*/
public ConcurrentLinkedQueue() {
head = tail = new Node<E>(null);
}
/**
* Creates a {@code ConcurrentLinkedQueue}
* initially containing the elements of the given collection,
* added in traversal order of the collection's iterator.
*
* @param c the collection of elements to initially contain
* @throws NullPointerException if the specified collection or any
* of its elements are null
*/
public ConcurrentLinkedQueue(Collection<? extends E> c) {
Node<E> h = null, t = null;
for (E e : c) {
checkNotNull(e);
Node<E> newNode = new Node<E>(e);
if (h == null)
h = t = newNode;
else {
t.lazySetNext(newNode);
t = newNode;
}
}
if (h == null)
h = t = new Node<E>(null);
head = h;
tail = t;
}
4. 入队
4.1 源码分析
因为是无界队列,add(e)
方法不用抛出异常。不支持添加 null。
java.util.concurrent.ConcurrentLinkedQueue#add
/**
* Inserts the specified element at the tail of this queue.
* As the queue is unbounded, this method will never throw
* {@link IllegalStateException} or return {@code false}.
*
* @return {@code true} (as specified by {@link Collection#add})
* @throws NullPointerException if the specified element is null
*/
public boolean add(E e) {
return offer(e);
}
入队的核心逻辑:
java.util.concurrent.ConcurrentLinkedQueue#offer
/**
* Inserts the specified element at the tail of this queue.
* As the queue is unbounded, this method will never return {@code false}.
*
* @return {@code true} (as specified by {@link Queue#offer})
* @throws NullPointerException if the specified element is null
*/
public boolean offer(E e) {
checkNotNull(e);
final Node<E> newNode = new Node<E>(e);
// 注意tail不一定是尾节点(甚至tail有可能存在于废弃的链上,后有解释),但是也不妨从tail节点开始遍历链表
for (Node<E> t = tail, p = t;;) { // 初始时t和p都指向tail节点
Node<E> q = p.next;
if (q == null) { // 使用p.next是否为空来判断p是否是尾节点,比较准确
// p is last node // 进入这里说明此时p是尾节点
if (p.casNext(null, newNode)) { // 若节点p的下一个节点为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
// 不管p与t是否相同,都应该casTail。但是这里只在p与t不同时才casTail,导致tail节点不总是尾节点,目的是减少对tail的CAS
casTail(t, newNode); // Failure is OK. // 将尾节点tail由t改为newNode,更新失败了也没关系,因为tail是不是尾节点不重要:)
return true;
}
// Lost CAS race to another thread; re-read next // CAS失败,说明其他线程先一步操作使得p的下一个节点不为null,需重新获取尾节点
}
else if (p == q) // 如果p的next等于p,说明p已经出队了,需要重新设置p、t的值
// 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.
// 1. 若节点t不再是tail,说明其他线程加入过元素(修改过tail),则取最新tail作为t和p,从新的tail节点继续遍历链表
// 2. 若节点t依旧是tail,说明从tail节点开始遍历链表已经不管用了,则把head作为p,从head节点从头遍历链表(注意这一步造成后续遍历中p!=t成立)
p = (t != (t = tail)) ? t : head;
// 这里没有更新tail,仍留在废链上
else
// Check for tail updates after two hops.
// 进入这里,说明p.next不为null,且p未出队,需要判断:
// 1. 若p与t相等,则t留在原位,p=p.next一直往下遍历(注意这一步造成后续遍历中p!=t成立)。
// 2. 若p与t不等,需进一步判断t与tail是否相等。若t不为tail,则取最新tail作为t和p;若t为tail,则p=p.next一直往下遍历。
// 就是说从tail节点往后遍历链表的过程,需时刻关注tail是否发生变化
p = (p != t && t != (t = tail)) ? t : q;
}
}
更新 tail 节点:
java.util.concurrent.ConcurrentLinkedQueue#casTail
private boolean casTail(Node<E> cmp, Node<E> val) {
return UNSAFE.compareAndSwapObject(this, tailOffset, cmp, val);
}
入队的基本思想:
- 从 tail 节点开始遍历到尾节点,若定位到尾节点(p.next == null),则入队。
- 遍历过程中,如果遍历到无效节点(p.next == p),需要重新从有效节点(tail 或 head)开始遍历。
- 遍历过程中,时刻关注 tail 节点是否无效。若无效了需要重新从最新的 tail 开始遍历,否则继续遍历当前的下一个节点。
需要注意的点:
- 入队过程中没有频繁执行 casTail(出队过程不会执行 casTail),因此 tail 位置有滞后,不一定指向尾节点,甚至可能位于废弃的链上。
- 使用 p.next == null 来判断尾节点,比使用 tail 准确。
- 通过 tail 遍历节点可能会遍历到无效节点,但是从 head 遍历总能访问到有效节点。
4.2 入队过程图示
执行offer(e)
入队,tail 并不总是指向尾节点,多个元素入队过程如下:
添加第一个元素(t 与 p 相等,不会更新 tail):
添加第二个元素(t 与 p 不相等,更新 tail):
添加第三个元素(t 与 p 相等,不会更新 tail):
4.3 tail 位于废弃链
由于出队 poll()
逻辑并不会执行 casTail()
来维护 tail 所在位置,因此 tail 可能滞后于 head,甚至位于废弃链上,如下图所示:
此时从 tail 往后遍历会访问到无效节点 p,该节点满足 p == p.next
。
如果想要继续访问到有效节点,需分两种情况:
- 从遍历开始至今,tail 的位置无变化,此时需要从 head 节点开始往下才能遍历到有效节点。
- 从遍历开始至今,tail 的位置发生了变化,说明其他线程更新了 tail 的位置,此时从新的 tail 开始往下遍历即可。
5. 出队
java.util.concurrent.ConcurrentLinkedQueue#poll
public E poll() {
restartFromHead:
for (;;) {
for (Node<E> h = head, p = h, q;;) { // 初始时h和p都指向head节点,从head节点开始遍历链表
E item = p.item;
if (item != null && p.casItem(item, null)) { // p.item不为空,把p节点的数据域设为空,返回p节点的数据
// Successful CAS is the linearization point
// for item to be removed from this queue.
if (p != h) // hop two nodes at a time
// 若p.next不为空,则把p.next设为头节点,把h和p出队;若p.next为空,则把p设为头节点,把h出队
updateHead(h, ((q = p.next) != null) ? q : p);
return item;
}
else if ((q = p.next) == null) { // 进入这里,说明p.item必然为空。若p.next也为空,说明队列中没有数据了,需要返回null
updateHead(h, p); // 把头节点设为p,把h出队
return null;
}
else if (p == q) // 如果p的next等于p,说明p已经出队了,重新从头节点开始遍历
continue restartFromHead;
else
p = q; // p = p.next 继续遍历链表
}
}
}
更新 head 节点:
java.util.concurrent.ConcurrentLinkedQueue#updateHead
/**
* Tries to CAS head to p. If successful, repoint old head to itself
* as sentinel for succ(), below.
*/
final void updateHead(Node<E> h, Node<E> p) {
if (h != p && casHead(h, p)) // 节点h和p不等,且当前头节点为h,则把头节点设为p
h.lazySetNext(h); // 原头节点h的next指向自身,表示h出队
}
出队的基本思想:
- 从 head 节点开始遍历找出首个有效节点(p.item != null),返回该节点的数据(p.item)。
- 遍历过程中,如果遍历到尾节点(p.next == null),则返回空。
- 遍历过程中,如果遍历到无效节点(p.next == p),说明其他线程修改了 head,需要重新从有效节点(新的 head)开始遍历。
需要注意的是,并不是每次出队时都执行 updateHead()
更新 head 节点:
- 当 head 节点里有元素时,直接弹出 head 节点里的元素,而不会更新 head 节点。
- 只有当 head 节点里没有元素时,出队操作才会更新 head 节点。
采用这种方式同样是为了减少使用 CAS 更新 head 节点的消耗,从而提高出队效率。
5.1 出队过程图示
场景一:队列中具有两个节点,头节点为无效节点。由于 p != h,此时需要把头节点出队。
场景二:队列中具有两个节点,头节点为有效节点。由于 p == h,此时不需要把头节点出队。
6. 容量
不用定义初始容量,无须扩容,容量最大值为 Integer.MAX_VALUE。
获取队列的容量:从头开始遍历队列中的有效节点,并计数。注意是遍历过程是弱一致的。
java.util.concurrent.ConcurrentLinkedQueue#size
public int size() {
int count = 0;
for (Node<E> p = first(); p != null; p = succ(p)) // 从第一个有数据的节点开始,一直遍历链表
if (p.item != null)
// Collection.size() spec says to max out
if (++count == Integer.MAX_VALUE) // 自增直到最大值
break;
return count;
}
java.util.concurrent.ConcurrentLinkedQueue#succ
/**
* Returns the successor of p, or the head node if p.next has been
* linked to self, which will only be true if traversing with a
* stale pointer that is now off the list.
*/
final Node<E> succ(Node<E> p) {
Node<E> next = p.next;
return (p == next) ? head : next; // 如果p已经出队了,则重新从头节点开始,否则继续遍历下一个节点
}
7. IDEA 调试的问题
编写单元测试,对 ConcurrentLinkedQueue#offer 进行调试。
@Test
public void test() {
ConcurrentLinkedQueue<String> queue = new ConcurrentLinkedQueue<>();
queue.offer("a");
queue.offer("b");
queue.offer("c");
queue.offer("d");
}
阅读源码可知入队过程并不会修改 head 节点,但是从 IDEA 的 debug 结果看到 head 节点发生了变化!
这是因为 IDEA 的 debug 过程会调用 ConcurrentLinkedQueue#toString 导致的。
ConcurrentLinkedQueue#toString 方法会创建迭代器,会调用到 ConcurrentLinkedQueue#first 方法,该方法会将首个有效节点作为头节点。
java.util.concurrent.ConcurrentLinkedQueue.Itr#Itr
java.util.concurrent.ConcurrentLinkedQueue.Itr#advance
java.util.concurrent.ConcurrentLinkedQueue#first
Node<E> first() { // 获取第一个具有非空元素的节点
restartFromHead:
for (;;) {
for (Node<E> h = head, p = h, q;;) {
boolean hasItem = (p.item != null);
if (hasItem || (q = p.next) == null) {
updateHead(h, p);
return hasItem ? p : null;
}
else if (p == q)
continue restartFromHead;
else
p = q;
}
}
}
关闭 IDEA debug 视图中自动调用 toString 方法:
重新打断点调试,可以看到入队后 head 节点不变:
8. 总结
- ConcurrentLinkedQueue 是非阻塞队列,采用 CAS 和自旋保证并发安全。
- ConcurrentLinkedQueue 的 tail 并不是严格指向尾节点,通过减少出队时对 tail 的 CAS 以提高效率。
- ConcurrentLinkedQueue 的 head 所指节点可能是空节点,也可能是数据节点,通过减少出队时对 head 的 CAS 以提高效率。
- 采用非阻塞算法,允许队列处于不一致状态(head/tail),通过保证不变式和可变式,来维护非阻塞算法的正确性。
- 由于是非阻塞队列,无法使用在线程池中。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。