[超级链接:Java并发学习系列-绪论]
[系列序章:Java并发43:并发集合系列-序章]
原文地址:https://www.jianshu.com/p/602b3240afaf
ConcurrentLinkedDeque 是双向链表结构的无界并发队列,从JDK 7开始加入到J.U.C的行列中,使用CAS实现并发安全。
与 ConcurrentLinkedQueue 的区别是该阻塞队列同时支持FIFO和FILO两种操作方式,即可以从队列的头和尾同时操作(插入/删除)。
ConcurrentLinkedDeque 适合“多生产,多消费”的场景。
内存一致性遵循:对 ConcurrentLinkedDeque 的插入操作先行发生于(happen-before)访问或移除操作。
相较于 ConcurrentLinkedQueue,ConcurrentLinkedDeque 由于是双端队列,所以在操作和概念上会更加复杂,来一起看下。
ConcurrentLinkedDeque(后面称CLD) 的实现方式继承了 ConcurrentLinkedQueue 和 LinkedTransferQueue的思想。
ConcurrentLinkedDeque 在非阻塞算法的实现方面与 ConcurrentLinkedQueue 基本一致。
关于 ConcurrentLinkedQueue,请参考笔者的上一篇文章:Java并发47:并发集合系列-基于CAS算法的非阻塞单向无界队列ConcurrentLinkedQueue。
重要属性:
//头节点
private transient volatile Node head;
//尾节点
private transient volatile Node tail;
//终止节点
private static final Node
和ConcurrentLinkedQueue一样,CLD 内部也只维护了head和tail属性,对 head/tail 节点也使用了“不变性”和“可变性”约束,不过跟 ConcurrentLinkedQueue 有些许差异,我们来看一下:
head/tail 的不变性:
head/tail的可变性:
除此之外,再来看看CLD中另外两个属性:
CLD的添加方法包括:offer(E)、add(E)、push(E)、addFirst(E)、addLast(E)、offerFirst(E)、offerLast(E)。
所有这些操作都是通过linkFirst(E)或linkLast(E)来实现的。
linkFirst(E) / linkLast(E)
/**
* Links e as first element.
*/
private void linkFirst(E e) {
checkNotNull(e);
final Node newNode = new Node(e);
restartFromHead:
for (;;)
//从head节点往前寻找first节点
for (Node h = head, p = h, q;;) {
if ((q = p.prev) != null &&
(q = (p = q).prev) != null)
// Check for head updates every other hop.
// If p == q, we are sure to follow head instead.
//如果head被修改,返回head重新查找
p = (h != (h = head)) ? h : q;
else if (p.next == p) // 自链接节点,重新查找
continue restartFromHead;
else {
// p is first node
newNode.lazySetNext(p); // CAS piggyback
if (p.casPrev(null, newNode)) {
// Successful CAS is the linearization point
// for e to become an element of this deque,
// and for newNode to become "live".
if (p != h) // hop two nodes at a time 跳两个节点时才修改head
casHead(h, newNode); // Failure is OK.
return;
}
// Lost CAS race to another thread; re-read prev
}
}
}
/**
* Links e as last element.
*/
private void linkLast(E e) {
checkNotNull(e);
final Node newNode = new Node(e);
restartFromTail:
for (;;)
//从tail节点往后寻找last节点
for (Node t = tail, p = t, q;;) {
if ((q = p.next) != null &&
(q = (p = q).next) != null)
// Check for tail updates every other hop.
// If p == q, we are sure to follow tail instead.
//如果tail被修改,返回tail重新查找
p = (t != (t = tail)) ? t : q;
else if (p.prev == p) // 自链接节点,重新查找
continue restartFromTail;
else {
// p is last node
newNode.lazySetPrev(p); // CAS piggyback
if (p.casNext(null, newNode)) {
// Successful CAS is the linearization point
// for e to become an element of this deque,
// and for newNode to become "live".
if (p != t) // hop two nodes at a time 跳两个节点时才修改tail
casTail(t, newNode); // Failure is OK.
return;
}
// Lost CAS race to another thread; re-read next
}
}
}
说明:
linkFirst是插入新节点到队列头的主函数,执行流程如下:
注意这里 CAS 指令成功后会判断 first 节点是否已经跳了两个节点,只有在跳了两个节点才会 CAS 更新 head,这也是为了节省 CAS 指令执行开销。
linkLast是插入新节点到队列尾,执行流程与linkFirst一致,不多赘述,具体见源码。
CLD的获取方法分两种:
pollFirst、pollLast包括了peekFirst 、peekLast的实现,都是找到并返回 first/last 节点。
不同的是,pollFirst、pollLast比peekFirst 、peekLast多了unlink这一步。
所以这里我们只对pollFirst和pollLast两个方法进行解析。
首先来看一下pollFirst() :
/**获取并移除队列首节点*/
public E pollFirst() {
for (Node p = first(); p != null; p = succ(p)) {
E item = p.item;
if (item != null && p.casItem(item, null)) {
unlink(p);
return item;
}
}
return null;
}
说明:
pollFirst()用于找到链表中首个 item 不为 null 的节点,并返回节点的item。
注意并不是first节点,因为first节点的item可以为null。
涉及的内部方法较多,不过都很简单,我们通过穿插代码方式分析:
1.首先通过first()方法找到first节点,first 节点必须为 active 节点(p.prev==null&&p.next!=p)。first()源码如下:
Node first() {
restartFromHead:
for (;;)
//从head开始往前找
for (Node h = head, p = h, q;;) {
if ((q = p.prev) != null &&
(q = (p = q).prev) != null)
// Check for head updates every other hop.
// If p == q, we are sure to follow head instead.
//如果head被修改则返回新的head重新查找,否则继续向前(prev)查找
p = (h != (h = head)) ? h : q;
else if (p == h
// It is possible that p is PREV_TERMINATOR,
// but if so, the CAS is guaranteed to fail.
//找到的节点不是head节点,CAS修改head
|| casHead(h, p))
return p;
else
continue restartFromHead;
}
}
2.如果first.item==null(这里是允许的,具体见上面我们对 first/last 节点的介绍),则继续调用succ方法寻找后继节点。succ源码如下:
/**返回指定节点的的后继节点,如果指定节点的next指向自己,返回first节点*/
final Node succ(Node p) {
// TODO: should we skip deleted nodes here?
Node q = p.next;
return (p == q) ? first() : q;
}
3.CAS 修改节点的 item 为 null(即 “逻辑删除-logical deletion”),然后调用unlink(p)方法解除节点链接,最后返回 item。
本章与 ConcurrentLinkedQueue 一篇中的非阻塞算法基本一致,只是为双端操作定义了几个可供操作的节点类型。