韩超的博客 (hanchao5272)

路漫漫其修远兮,吾将上下而求索。

Java并发48:并发集合系列-基于CAS算法的非阻塞双向无界队列ConcurrentLinkedDueue

[超级链接: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<E> head;
//尾节点
private transient volatile Node<E> tail;
//终止节点
private static final Node<Object> PREV_TERMINATOR, NEXT_TERMINATOR;
//移除节点时更新链表属性的阀值
private static final int HOPS = 2;

和ConcurrentLinkedQueue一样,CLD 内部也只维护了head和tail属性,对 head/tail 节点也使用了“不变性”和“可变性”约束,不过跟 ConcurrentLinkedQueue 有些许差异,我们来看一下:

head/tail 的不变性:

  • 第一个节点总是能以O(1)的时间复杂度从 head 通过 prev 链接到达;
  • 最后一个节点总是能以O(1)的时间复杂度从 tail 通过 next 链接到达;
  • 所有live节点(item不为null的节点),都能从第一个节点通过调用 succ() 方法遍历可达;
  • 所有live节点(item不为null的节点),都能从最后一个节点通过调用 pred() 方法遍历可达;
  • head/tail 不能为 null;
  • head 节点的 next 域不能引用到自身;
  • head/tail 不会是GC-unlinked节点(但它可能是unlink节点)。

head/tail的可变性:

  • head/tail 节点的 item 域可能为 null,也可能不为 null;
  • head/tail 节点可能从first/last/tail/head 节点访问时不可达;
  • tail 节点的 next 域可以引用到自身。

除此之外,再来看看CLD中另外两个属性:

  • PREV_TERMINATOR:prev的终止节点,next指向自身,即PREV_TERMINATOR.next = PREV_TERMINATOR。在 first 节点出列后,会把first.next指向自身(first.next=first),然后把prev设为PREV_TERMINATOR。
  • NEXT_TERMINATOR:next的终止节点,prev指向自身,即NEXT_TERMINATOR.pre = NEXT_TERMINATOR。在 last 节点出列后,会把last.prev指向自身(last.prev=last),然后把next设为NEXT_TERMINATOR。

添加(入列)

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<E> newNode = new Node<E>(e);

    restartFromHead:
    for (;;)
        //从head节点往前寻找first节点
        for (Node<E> 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<E> newNode = new Node<E>(e);

    restartFromTail:
    for (;;)
        //从tail节点往后寻找last节点
        for (Node<E> 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是插入新节点到队列头的主函数,执行流程如下:

  • 首先从 head 节点开始向前循环找到 first 节点(p.prev==null&&p.next!=p);
  • 然后通过lazySetNext设置新节点的 next 节点为 first;
  • 然后 CAS 修改 first 的 prev 为新节点。

注意这里 CAS 指令成功后会判断 first 节点是否已经跳了两个节点,只有在跳了两个节点才会 CAS 更新 head,这也是为了节省 CAS 指令执行开销。

linkLast是插入新节点到队列尾,执行流程与linkFirst一致,不多赘述,具体见源码。


获取(出列)

CLD的获取方法分两种:

  • 获取节点:peek、peekFirst 、peekLast、getFirst、getLast,都是通过peekFirst 、peekLast实现。
  • 获取并移除节点: poll、pop、remove、pollFirst、pollLast、removeFirst、removeLast,都是通过pollFirst、pollLast实现。

pollFirst、pollLast包括了peekFirst 、peekLast的实现,都是找到并返回 first/last 节点。

不同的是,pollFirst、pollLastpeekFirst 、peekLast多了unlink这一步。

所以这里我们只对pollFirst和pollLast两个方法进行解析。

首先来看一下pollFirst()

/**获取并移除队列首节点*/
public E pollFirst() {
    for (Node<E> 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<E> first() {
    restartFromHead:
    for (;;)
        //从head开始往前找
        for (Node<E> 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<E> succ(Node<E> p) {
    // TODO: should we skip deleted nodes here?
    Node<E> q = p.next;
    return (p == q) ? first() : q;
}

3.CAS 修改节点的 item 为 null(即 “逻辑删除-logical deletion”),然后调用unlink(p)方法解除节点链接,最后返回 item。

小结

本章与 ConcurrentLinkedQueue 一篇中的非阻塞算法基本一致,只是为双端操作定义了几个可供操作的节点类型

阅读更多
个人分类: Java并发学习总结
所属专栏: Java并发学习实例
想对作者说点什么? 我来说一句

没有更多推荐了,返回首页

不良信息举报

Java并发48:并发集合系列-基于CAS算法的非阻塞双向无界队列ConcurrentLinkedDueue

最多只允许输入30个字

加入CSDN,享受更精准的内容推荐,与500万程序员共同成长!
关闭
关闭