ConcurrentLinkedQueue源码解析
文章目录
一、ConcurrentLinkedQueue简介及基本结构
ConcurrentLinkedQueue是可以用来实现高并发的无界线程安全队列,底层是一个单向链表。它采用非阻塞算法,通过循环CAS保证了入队、出队等操作的线程安全。
ConcurrentLinkedQueue中的Node节点如下所示,其item和next属性都是多线程可见的(通过volatile关键字)。对Node的操作,不论是设置item值还是设置next,都是通过CAS算法保证线程安全。对于compareAndSwapObject()方法,cmp表示期望值,val表示设置目标值,只有当前值和cmp相等时,才能设置成功。
// Node节点
private static class Node<E> {
volatile E item;
volatile Node<E> next;
// CAS设置item
boolean casItem(E cmp, E val) {
return UNSAFE.compareAndSwapObject(this, itemOffset, cmp, val);
}
// CAS设置next
boolean casNext(Node<E> cmp, Node<E> val) {
return UNSAFE.compareAndSwapObject(this, nextOffset, cmp, val);
}
}
对于ConcurrentLinkedQueue底层链表,有一个头结点head和尾节点tail用于进行链表队列的管理。
// 头结点
private transient volatile Node<E> head;
// 尾节点
private transient volatile Node<E> tail;
// 默认构造函数,头结点和尾节点初始化在一起
public ConcurrentLinkedQueue() {
head = tail = new Node<E>(null);
}
二、ConcurrentLinkedQueue的常用方法解析
2.1 入队方法offer()
入队方法offer()采用尾插法,没有任何锁操作,线程安全完全由CAS和队列算法保证。
public boolean offer(E e) {
checkNotNull(e);
final Node<E> newNode = new Node<E>(e);
// 大CAS,不成功就会一直循环
for (Node<E> t = tail, p = t;;) {
Node<E> q = p.next;
// p是最后一个节点的情况
if (q == null) {
// 直接进行 CAS 尾插
if (p.casNext(null, newNode)) {
// 每两次更新一下 tail
if (p != t)
casTail(t, newNode);
return true;
}
// CAS如果不成功,会循环尝试
}
// 处理哨兵节点情况
else if (p == q)
p = (t != (t = tail)) ? t : head;
// p不是最后一个节点的情况
else
// 在两次添加之后检查尾部更新,即更新 p节点指向尾节点
// 不是原子操作,代表取下一个或最后一个节点
p = (p != t && t != (t = tail)) ? t : q;
}
}
总结 :
- offer()的主要任务一是将入队节点进行线程安全地尾插,二是更新tail节点。
- 对于tail节点的更新,是存在拖延现象的,它并不总是在更新,事实上,tail节点每两次入队后更新一次(可以总结为入队的是偶数个节点时,才会更新tail)。
- for循环是核心,没有出口,直到CAS尝试成功。
- 如果 tail是指向尾节点的 (q == null),直接进入第一个分支,然后采用 CAS 实现新节点的尾插。此时(p == t)是成立的,因此 不会进行tail的更新。
- 如果 tail不是指向尾节点的,会先进入最后一个分支,开始查找最后一个元素的位置。此时p会更新为指向最后一个元素。在第二次循环时,会进入第一个分支(q = p.next == null),但由于此时p的指向和tail不是一个节点,会利用 CAS 更新tail节点为最后一个节点。
- 对于 第三个分支的更新操作 ,主要是考虑到了多线程时的线程安全更新问题。由于 != 不是原子操作,所以在并发比较的过程中,可能出现中断问题,导致尾节点先被其它线程更新了。即在获得左边的t后,右边的被其它线程修改(t != t成立)。此时由于尾节点tail被篡改,则以新的tail为尾节点。
- 第二个分支用于处理哨兵节点情况(即next指向了自己),它主要是已经被删除的节点,因为无法从next获取下一个节点,直接返回head从头遍历。但是也考虑了线程安全问题,中途如果tail被改变(t != t),直接使用新的tail。哨兵节点的产生在poll()方法中介绍。
如果每次都使用循环CAS更新tail节点,影响性能,所以采用延迟更新的方式减少CAS更新tail节点的次数,提高入队的效率。下图是入队的基本演示(图片来源于此):
2.2 出队方法poll()
出队方法poll()采用头删法,会产生哨兵节点。图片来源于此:
public E poll() {
restartFromHead:
for (;;) {
// p节点指向需要出队的节点
for (Node<E> h = head, p = h, q;;) {
E item = p.item;
// 如果 p 不为 null,则通过 CAS 来循环设置 p 指向的元素为null,成功则返回删除的元素值
if (item != null && p.casItem(item, null)) {
// 每两次更新 head 指向
if (p != h)
updateHead(h, ((q = p.next) != null) ? q : p);
return item;
}
// 如果第一个节点(p.next)的元素为空,则已被其它线程修改
// 直接更新头结点
else if ((q = p.next) == null) {
updateHead(h, p);
return null;
}
// 遇到哨兵节点,直接从新的 head 开始查找
else if (p == q)
continue restartFromHead;
// head 的 item 为 null,p 指向 p.next,即第一个元素
else
p = q;
}
}
}
// 更新头节点
final void updateHead(Node<E> h, Node<E> p) {
// p 被设置为新的 head
if (h != p && casHead(h, p))
// h 的next 指向自己,成为哨兵
h.lazySetNext(h);
}
void lazySetNext(Node<E> val) {
// 存储变量的引用到对象的指定的偏移量处
// 有延迟,非立即可见
UNSAFE.putOrderedObject(this, nextOffset, val);
}
总结:
- 同尾插一样,头删时,head的更新也是有拖延现象的,每两次删除更新一下。
- 核心思想同样是for循环,采用CAS进行更新。
- 如果head的item为null(比如刚开始时,或者未更新),进入最后一个分支,p == q(p.next)直接指向下一个节点,即队列的第一个元素。
- 如果如果 p 不为 null,进入第一个分支,通过 CAS 来循环设置 p 指向的元素为null,成功则返回删除的元素值。如果这是第二次大循环,即从步骤3过来的,会有p != h,直接CAS更新head。
- 如果队列的第一个节点(p.next)的元素为空,则已被其它线程修改,此时进入第二个分支,直接更新头结点。
- 遇到哨兵节点,进入第三个分支,直接从新的 head 开始查找。
- 对于updateHead方法,原有的head成为了哨兵,此时head和tail就就指向同一个元素,不论是offer()方法,还是poll()方法,都有可能访问到这个哨兵节点。