什么是ConcurrentLinkedQueue?
ConcurrentLinkedQueue是Java并发包里的一个线程安全的无界非阻塞队列,基于链表实现,采用CAS算法保证并发安全。
其特点有如下:
-
数据结构:单向链表实现链表队列
-
线程安全:基于
CAS实现的线程安全 -
是否阻塞:基于
CAS实现,因此不会阻塞 -
是否有界:使用链表实现,无界,只受内存容量的限制
为什么要创造ConcurrentLinkedQueue?
在ConcurrentLinkedQueue出现之前,我们需要同步队列的时候使用synchronized与ArrayList等队列同步配合。但是加锁的消耗太大,在高并发的情况下,就会影响性能,ConcurrentLinkedQueue就是为了取代这类基于锁的同步队列方案,让我们在不阻塞线程的前提下,依然能安全地操作队列。
内部数据结构分析
Node节点
static final class Node<E> {
volatile E item;
volatile Node<E> next;
Node(E item) {
ITEM.set(this, item);
}
/** Constructs a dead dummy node. */
Node() {}
void appendRelaxed(Node<E> next) {
// assert next != null;
// assert this.next == null;
NEXT.set(this, next);
}
boolean casItem(E cmp, E val) {
// assert item == cmp || item == null;
// assert cmp != null;
// assert val == null;
return ITEM.compareAndSet(this, cmp, val);
}
}
这个Node类是ConcurrentLinkeQueue的核心静态内部类。
它只有两个数据:item以及next,并且都使用了volatile进行修饰。
Node(E item) { ITEM.set(this, item); }在有参构造中,使用的代码非常奇怪,不是常规的
this.item=item,而是利用VarHandle来设置初值。
原因如下:
item是volatile字段,直接使用this.item=item会触发volatile写,JVM会强制插入内存屏障(StoreStore与StoreLoad),确保item的立即可见,但是这会带来额外的开销。- 使用
VarHandle.set()是一种relaxed write,不强制全局可见性,Node对象即便是在构造之后并不会被其他线程可见也没有关系,因为它并不会立即被其他线程访问,而是要链接到队列之后才必须能够被其他线程访问,同时链接到队列的方法是通过CAS来操作的,CAS可以保证可见性。
因此这是一种依赖CAS搭便车的机制:
- 节点对象只有在通过
CAS链接到队列之后才会被其他线程感知。 CAS本身就具备volatile语义,因此,在CAS链接到队列之后的任何对item的写入,即便是非volatile写,也会随着CAS的成功而对其他线程可见。- 因为有
CAS兜底,因此就完全没有必要插入内存屏障,带来额外的开销。
综上,这是一种无锁并发的优化手段,将对象的可见性推迟到真正需要的时候,从而减少不必要的同步成本。
构造方法
public ConcurrentLinkedQueue() {
head = tail = new Node<E>();
}
以上是ConcurrentLinkedQueue的无参构造方法,很明显它使用了一个dummy节点,来简化链表的操作算法,并且初始时让头节点和尾节点都指向它。
ConcurrentLinkedQueue操作
offer操作
public boolean offer(E e) {
final Node<E> newNode = new Node<E>(Objects.requireNonNull(e));
for (Node<E> t = tail, p = t;;) {
Node<E> q = p.next;
if (q == null) {
// p is last node
if (NEXT.compareAndSet(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; failure is OK
TAIL.weakCompareAndSet(this, t, newNode);
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;
}
}
offer方法只要不提供NULL,因为CLQ使用链表存储,是无界队列,因此一定返回true。
- 检查对象是否为
NULL,并且包装成节点。 - 用
t记录当前的尾节点,并用p记录当前尝试连接的位置,初始是当前的尾节点- 如果
p的后继节点是NULL,则尝试CAS将节点加入,当节点加入成功后,再判断p是不是之前记录的尾节点,如果不是,则尝试更新尾节点。 - 如果
p的后继节点是p,说明p已经出队了(节点出队时会自引用),此时如果尾节点已经变了,就更新t,并且p重新从t出发,如果尾节点没变,将p从头节点开始。 - 最后的情况是
p的后继不是NULL,但是p也没有出队,此时如果p不是原tail同时tail被更新,更新t,将p跳转到新的tail。否则p前进。
- 如果
CLQ在多线程的情况下将节点入队时,tail并不总是真正的尾节点,这也是为什么需要p从头节点开始的原因。
poll操作
poll操作弹出第一个元素,如果队列为空则返回null。
- 整体使用
goto语句。 - 从头节点
head开始,初始状态h、p都指向head- 利用局部变量保存
p的item,当p的item不等于空的时候,使用cas将其置为null,如果p不等于h,说明p已经移动过了,此时更新头节点 - 如果
p的后继q是空,则返回空 - 如果
p和q相等,说明p已经被弹出,需要重新循环。
- 利用局部变量保存
当节点出队的时候,会自引用自身。