ConcurrentLinkedQueue
JDK中提供了一系列场景的并发安全队列。总的来说,按照事先方式的不同可分为阻塞队列和非阻塞队列,前者使用锁实现,后者则使用CAS非阻塞算法实现。
ConcurrentLinkedQueue是线程安全的无解非阻塞队列,其底层数据结构使用单向链表实现,对于入队和出队操作使用CAS来实现线程安全。
ConcurrentLinkedQueue内部的队列使用单向链表方式实现,其中有两个volatile类型的Node节点分别用来存放队列的首、尾节点。从下面的无参构造函数可知,默认头、尾节点都是指向item为null的哨兵节点。新元素会被插入队列末尾,出队时从队列头部获取一个元素。
public ConcurrentLinkedQueue() {
head = tail = new Node<E>(null);
}
其内部则使用UNSafe工具类提供的CAS算法来保证出入队时操作链表的原子性。
1、offer操作
offer是在队列末尾添加一个元素,如果传递的参数是null则抛出NPE异常,否则由于 ConcurrentLinkedQueue是无界队列,该方法一直会返回true。另外,由于使用CAS无阻塞算法,因此该方法不会阻塞挂起调用线程。
public boolean offer(E e) {
// (1)e为null则抛出空指针异常
checkNotNull(e);
// (2)构造Node节点,在构造函数内部调用unsafe.putObject
final Node<E> newNode = new Node<E>(e);
// (3)从尾结点进行插入
for (Node<E> t = tail, p = t;;) {
Node<E> q = p.next;
// (4)如果q==null说明p是尾结点则执行插入
if (q == null) {
// p is last node (5) 使用CAS设置p节点的next节点
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".
// (6)CAS成功,则说明新增节点已经被放入链表,然后设置当前尾结点(包含head,第1,3,5.。。个节点为尾结点)
if (p != t) // hop two nodes at a tim
casTail(t, newNode); // Failure is OK.
return true;
}
// Lost CAS race to another thread; re-read next
}
else if (p == q)
// (7)多线程操作时,由于poll操作移除元素后可能会把head变为自引用,也就是head的next变成了head,所以这里需要找新的head
// 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.
// (8)寻找尾结点
p = (p != t && t != (t = tail)) ? t : q;
}
}
单个线程调用offer方法的运行流程:
- 首先对传参进行空参检查
- 执行代码(2)并使用item作为构造函数参数创建一个新的节点
- 执行代码(3)从队列尾部节点开始循环,打算从队列尾部添加元素
- 当执行代码(4)时队列状态如图所示,这是p,t,head,tail同时指向了item为null 的哨兵节点,由于哨兵节点的next节点为null,所以这里q也指向null。代码(4)发现q==null则执行代码(5)
- 代码(5)通过CAS原子操作判断p节点的next节点是否为unll,如果为null则使用节点newNode替换p的next,然后执行代码(6)
- 这里由于p==t所以没有设置尾部节点,然后退出offe方法,这是队列的状态如图
多个线程调用offer方法的运行流程:
- 假设有两个线程A,B同时执行到代码(5)p.casNext(null, newNode)。由于CAS的比较设置操作是原子性的,所以这里假设线程A先执行了比较设置操作,发现当前p的next节点确实是null,则会原子性的更新next节点为item1,这时候线程B也会判断p的next节点是否为null,结果发现不是null(因为线程A已经设置了p的next节点为item1),则会调到代码(3),然后执行代码(4)
- 根据上面状态可知线程B接下来会执行代码(8),然后把q赋予给p,这时候队列状态如图所示
- 然后线程再次跳转到(3),当执行到(4)时状态如图
- 此时用于q==null,所以线程B会执行代码(5),通过CAS操作判断当前p的next节点是否为null,不是则再次循环尝试,是则用item2替换。假设CAS成功了,那么执行代码(6),由于p!=他,所以设置tail节点为item2,然后退出offer方法
代码(7)要在poll操作后才会执行。
可见,offer操作中的关键步骤是代码(5),通过原子CAS操作来控制某时只有一个线程可以追加元素到队列末尾。通过CAS竞争失败的线程会通过循环一次次尝试进行CAS操作,知道CAS操作成功才返回,也就是通过使用无限循环不断进行CAS尝试方法来替代阻塞算法挂起调用线程。相比阻塞算法,这是使用CPU原换取阻塞锁带来的开销。
2、add操作
add操作是在链表末尾添加一个元素,其实在内部调用的还是offer操作
public boolean add(E e) {
return offer(e);
}
3、poll操作
poll操作是在队列同步获取并移除一个元素,如果队列为空则返回null。下面看看实现原理
public E poll() {
// (1)goto标记
restartFromHead:
// (2)无限循环
for (;;) {
for (Node<E> h = head, p = h, q;;) {
// (3)保存当前节点值
E item = p.item;
// (4)当前节点右值则CAS变为NULL
if (item != null && p.casItem(item, null)) {
// Successful CAS is the linearization point
// for item to be removed from this queue.
// (5)CAS成功则标记当前节点并从链表中移除
if (p != h) // hop two nodes at a time
updateHead(h, ((q = p.next) != null) ? q : p);
return item;
}
// (6)当前队列为空则返回null
else if ((q = p.next) == null) {
updateHead(h, p);
return null;
}
// (7)如果当前节点被自引用了,则重新寻找新的队列头结点
else if (p == q)
continue restartFromHead;
else
// (8)
p = q;
}
}
}
final void updateHead(Node<E> h, Node<E> p) {
if (h != p && casHead(h, p))
h.lazySetNext(h);
}
poll方法在移除一个元素时,只是简单地使用CAS操作把当前节点的item值设置为null,然后通过重新设置头结点将该元素从队列里面移除,被移除的节点就变成孤立节点,这个节点会在垃圾回收时被回收掉。另外,如果在执行分支中发现头结点被修改了,要跳到外层循环重新获取新的头结点。
4、peek操作
peek操作是获取队列头部一个元素(只获取不移除),如果队列为空则返回null
public E peek() {
//(1)
restartFromHead:
for (;;) {
for (Node<E> h = head, p = h, q;;) {
// (2)
E item = p.item;
// (3)
if (item != null || (q = p.next) == null) {
updateHead(h, p);
return item;
}
// (4)
else if (p == q)
continue restartFromHead;
else
// (5)
p = q;
}
}
}
peek操作的代码结构与poll类似,不同之处在于(3)处少了castItem操作。其实这很正常,因为peek只是获取对流头元素值,并不清空其值。根据前面的介绍我们知道第一次执行offer后head指向的是哨兵节点(也就是item为null的节点),那么第一次执行peek时在代码(3)中会发现item==null,然后执行q = p.next,这时候q节点指向的才是队列里面第一个真正的元素,获取如果队列为null则q指向null。
peek操作的代码与poll代码类似,只是前者指获取对流头元素电商部并不从队列里将它删除,而后者获取后需要从队列里将它删除。另外,在第一次调用peek操作时,会删除哨兵节点,并让队列的head节点指向队列里面第一个元素或者null。
5、size操作
计算当前队列元素个数,在并发环境下不是很有用,因为CAS没有加锁,所以从调用size函数到返回结果期间有可能增删元素,导致统计的元素个数不精确。
6、remove操作
如果队列里面存在该元素则删除该元素,如果存在多个则删除第一个,并返回true,否则返回false
public boolean remove(Object o) {
// 为空则直接返回false
if (o != null) {
Node<E> next, pred = null;
for (Node<E> p = first(); p != null; pred = p, p = next) {
boolean removed = false;
E item = p.item;
if (item != null) {
if (!o.equals(item)) {
next = succ(p);
continue;
}
removed = p.casItem(item, null);
}
next = succ(p);
if (pred != null && next != null) // unlink
pred.casNext(p, next);
if (removed)
return true;
}
}
return false;
}
7、contains操作
按断队列里面是否含有指定对象,由于是遍历整个队列,所以想size操作一样结果也不精确,有可能调用该方法时元素还在队列里面,但是遍历过程中其它线程才把该元素删除了,那么就会返回false。
小结
ConcurrentLinkedQueue的底层使用单向链表数据结构来把偶才能队列元素,每个元素被包装成一个Node节点。队列靠头、尾节点来维护,创建队列时头、为节点指向一个item为null的哨兵节点。第一次执行peek或者first操作时会把head指向第一个真正的队列元素。由于使用非阻塞CAS算法,所以在计算size时有可能进行了offer、poll或者remove操作,导致计算的元素个数不精确,所以在并发情况下size函数不是很有用。