简介

ConcurrentLinkedQueue是JUC包下的线程安全的无界非阻塞队列,它与BlockingQueue接口实现类最大的不同就是,BlockingQueue是阻塞队列,而ConcurrentLinkedQueue是非阻塞队列。这里的阻塞非阻塞,指的是队列满了或为空的时候,线程移除或放入元素的时候,是否需要阻塞挂起。BlockingQueue底层是用锁实现的,而ConcurrentLinkedQueue底层使用CAS实现的。

实现原理

先来看重要的属性和数据结构:

// 头结点
private transient volatile Node<E> head;
// 尾结点
private transient volatile Node<E> tail;
// 初始化头、尾节点为哑节点
public ConcurrentLinkedQueue() {
    head = tail = new Node<E>(null);
}

其中,Node是它的内部类:

private static class Node<E> {
    volatile E item;
    volatile Node<E> next;
    Node(E item) {
        UNSAFE.putObject(this, itemOffset, item);
    }
    // CAS方式修改当前结点的元素
    boolean casItem(E cmp, E val) {
        return UNSAFE.compareAndSwapObject(this, itemOffset, cmp, val);
    }
    // 延迟设置当前结点的后继结点
    void lazySetNext(Node<E> val) {
        // 有序、延迟版本的putObjectVolatile方法,不保证值的改变被其他线程立即可见。只有在field被volatile修饰时有效
        UNSAFE.putOrderedObject(this, nextOffset, val);
    }
    // CAS方式设置当前结点的后继结点
    boolean casNext(Node<E> cmp, Node<E> val) {
        return UNSAFE.compareAndSwapObject(this, nextOffset, cmp, val);
    }
    // Unsafe mechanics
    private static final sun.misc.Unsafe UNSAFE;
    private static final long itemOffset;
    private static final long nextOffset;
    static {
           try {
               UNSAFE = sun.misc.Unsafe.getUnsafe();
               Class<?> k = Node.class;
               itemOffset = UNSAFE.objectFieldOffset
                   (k.getDeclaredField("item"));
               nextOffset = UNSAFE.objectFieldOffset
                   (k.getDeclaredField("next"));
            } catch (Exception e) {
               throw new Error(e);
            }
    }
}

不难看出,ConcurrentLinkedQueue是用单向链表实现的。

再来看它的重要方法实现:
offer操作

public boolean offer(E e) {
    // e为空则抛空指针
    checkNotNull(e);
    // 构造待插入的元素结点
    final Node<E> newNode = new Node<E>(e);
    // 多线程环境下,从尾结点处,循环尝试插入
    for (Node<E> t = tail, p = t;;) {
        Node<E> q = p.next;
        if (q == null) {
               // q为null说明p是尾结点,尝试CAS插入
               // p is last node
               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". 
                   // CAS操作成功
                   // 每隔1个节点,更新tail节点为新插入的节点,包含哑节点在内,第1,3,5,7..个节点设置为尾节点,也就是说这里的tail节点并不总是指向最后一个节点,理解这一点很重要。
                   if (p != t) // hop two nodes at a time
                      casTail(t, newNode); // Failure is OK.
                   return true;
               }
            // Lost CAS race to another thread; re-read next
        }
        // p == q成立的场景在于,多线程环境下,其他线程调用了poll移除头部元素的操作,使得head自引用,即head.next==head,这时需要重新找新的head。可能还是不好理解,结合下面的poll()方法注释就清楚了
        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 
            // 入队操作被其他线程抢先了,需要让p定位最新的尾节点,再去循环尝试CAS操作,直到成功为止。
            // Check for tail updates after two hops.
            // 这里的t != (t = tail)意思是检测tail是否发生变化,上面提到tail指向的是1,3,5,7...号节点,如果tail发生了变化,则更新t的值。 
            p = (p != t && t != (t = tail)) ? t : q;
 }
}

offer()方法总结:
offer的关键步骤是用CAS操作尝试将newNode插入到队列尾部,如果CAS失败了,则定位到最新的尾结点,并循环重试,直到CAS成功。在多线程环境下,采用自璇的方式替代了锁阻塞方式,因此性能会提升,但也消耗了CPU资源。

再看poll操作:

public E poll() {
    // goto标记
    restartFromHead:
    // 无限循环
    for (;;) {
        // 指针p从头结点开始
        for (Node<E> h = head, p = h, q;;) {
            // 保存当前元素
            E item = p.item;
            // 当前指向的结点元素不为空,则CAS方式尝试将当前结点元素置为null
            if (item != null && p.casItem(item, null)) {
                // Successful CAS is the linearization point
                // for item to be removed from this queue. 
                  // CAS操作成功,并且每隔1个节点,更新头结点,即头结点指向的是哑节点或者第一个元素结点。与offer()更新尾结点有点类似。
                  if (p != h) // hop two nodes at a time
                  updateHead(h, ((q = p.next) != null) ? q : p);
                  // 返回被移除的当前元素
                  return item;
            }
            // 当前队列为空,则返回null
            else if ((q = p.next) == null) {
                updateHead(h, p);
                return null; 
            }
            // 如果当前结点自引用了,则重新从最新的头结点开始下一轮尝试
            else if (p == q)
                continue restartFromHead;
            // 将p移动到下一个结点,目的是找到第一个元素不为null的节点,然后进行CAS尝试
            else p = q;
        }
    }
}

其中updateHead代码如下:

final void updateHead(Node<E> h, Node<E> p) {
    if (h != p && casHead(h, p))
        // 更新头结点成功,则把旧的节点自引用,即h.next = h
        // 目的是把它从链表中脱离出来,达到移除元素的效果,它最终会被垃圾回收。
        h.lazySetNext(h);
}

poll()方法总结:
poll方法在移除头部元素时,是通过CAS操作将当前结点的元素值置为null,然后重新设置头结点,来达到移除元素的目的,被移除的结点会自引用,成为孤儿结点被垃圾回收掉。另外,如果在循环尝试过程中发现头结点发生了变化,会调到外层循环restartFromHead,从最新的head重新开始。

peek操作:

public E peek() {
    restartFromHead:
    for (;;) {
        for (Node<E> h = head, p = h, q;;) {
            E item = p.item;
            if (item != null || (q = p.next) == null) {
                updateHead(h, p);
                return item;
            }
            else if (p == q)
                continue restartFromHead;
            else p = q;
        }
    }
}

可以看到,peek方法结构和poll差不多,只不过没有移除元素的操作,这也是合理的,因为peek操作只是查看队列头部元素。

size()方法:

public int size() {
    int count = 0;
    for (Node<E> p = first(); p != null; p = succ(p))
        if (p.item != null)
            // Collection.size() spec says to max out
           if (++count == Integer.MAX_VALUE)
                break;
    return count;
}

要注意的是,这个类的size()方法不像大多数集合那样,它的size不是强一致性的,因为在调用size()方法时,在返回count前,可能有其他线程调用了offer或者poll方法,所以在多线程环境下,size()方法返回的队列长度并不总是精确,我们在使用时需要注意这点。

first()方法:

Node<E> first() {
    restartFromHead:
    for (;;) {
        for (Node<E> h = head, p = h, q;;) {
            boolean hasItem = (p.item != null);
            if (hasItem || (q = p.next) == null) {
                updateHead(h, p);
                return hasItem ? p : null;
            }
            else if (p == q)
                continue restartFromHead;
            else p = q;
        }
    }
}

注意,first()与peek()方法返回值不同,first()返回的是结点Node<E>,而peek()返回的是元素E。first()返回的是第一个元素结点,哑节点不算在内,即如果队列为空则返回null。并且,first()不是public的,默认是包内访问权限,它通常在类的内部其他方法调用,我们应用程序无法直接调用。

remove(Object o)方法:

public boolean remove(Object o) {
    // o为null时直接跳到最后return false。
    if (o != null) {
        // 记录当前遍历指针p的后继结点next和前继结点pred,移除时会用到
        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; 
                }
                // 找到目标元素,尝试CAS操作将元素置为null
                removed = p.casItem(item, null);
            }
            next = succ(p);
            // 将目标元素的前继结点和后继结点用CAS方式连接起来,达到移除目标元素的目的
            if (pred != null && next != null) // unlink
                pred.casNext(p, next);
            // 移除成功返回true,否则继续往后遍历链表尝试移除目标元素  
            if (removed)
                return true;
            }
    }
    return false;
}

参考资料:
《Java并发编程之美》


小强大人
34 声望4 粉丝