ConcurrentLinkedQueue

523 阅读5分钟

前面我们分析了ArrayList的并发替代品CopyOnWriteArrayList。它使用了写时复制的策略来提高并发问题。

今天我们研究下LinkedList的并发替代品ConcurrentLinkedQueue. 并发链表。

要实现并发安全,一般2种策略。一种是加锁,一种是cas。那么我们看下ConcurrentLinkedQueue用的哪种策略。

我们先看下它的类结构。


该类内部定义了一个Node类。拥有item(存的元素),next(下一节点)。其中我们看到一个Unsafe属性。
Unsafe提供了很多方法直接对内存进行读和写操作。其中大部分方法很底层,对应到了硬件指令。效率比较高。它提供一个Unsafe.getUnsafe()方法来获取unsafe实例。
但只能在JDK中的源码中使用,因为JDK之外的代码被认为是不被信任的,因此不能通过这种方式使用Unsafe。
但可以通过反射来使用它。

Field f = Unsafe.class.getDeclaredField("theUnsafe");
f.setAccessible(true);
Unsafe unsafe = (Unsafe) f.get(null);

虽然如此,但我们还是不建议在应用代码里使用Unsafe的。因为对内存的操作是非常危险的。Unsafe中有以下常用方法 objectFieldOffset:给出指定字段的偏移量,这个偏移量是不变的,同一个类中不同的字段不会存在相同的偏移量; putObject:存储一个值到指定变量; compareAndSwapObject:如果当前值是期望的值,则原子的更新该值到新值; putOrderedObject:存储值到指定字段,但不提供可见性,如果需要具备可见性,则需要指定字段为volatile。

其中,add方法将指定元素插入队列的尾部。poll获取并移除队列的头节点。如果队列为null,则返回null。

add方法源码分析

public boolean add(E e) {
        return offer(e);
    }  
public boolean offer(E e) {
       //检测是否为null,若为null,抛npe
        checkNotNull(e);
        //用cas创建一个对象
        final Node<E> newNode = new Node<E>(e);
        for (Node<E> t = tail, p = t;;) {
            Node<E> q = p.next;
            if (q == null) {
                if (p.casNext(null, newNode)) {
                    if (p != t) 
                        casTail(t, newNode); 
                    return true;
                }
            }
            else if (p == q)
                p = (t != (t = tail)) ? t : head;
            else
                p = (p != t && t != (t = tail)) ? t : q;
        }
    }

下面是jdk1.6版本

public boolean add(E e) {  
    return offer(e);  
}  
public boolean offer(E e) {  
    if (e == null) throw new NullPointerException();  
    Node<E> n = new Node<E>(e);  
    retry:  
    for (;;) {  
    //创建一个tail节点的引用
        Node<E> t = tail;  
        //p表示尾节点,默认等于tail节点
        Node<E> p = t;  
        for (int hops = 0; ; hops++) {  
        //1.获取p的后继节点。(如果p的next指向自身,返回head节点) 
            Node<E> next = succ(p); 
            2.如果next不为null ,说明不是尾节点。更新p为next节点 
            if (next != null) { 
                if (hops > HOPS && t != tail)   
                    continue retry;   
                p = next;  4.如果自旋字数小于HOPS或者t是尾节点,将p指向next。
               //5.如果next为null,尝试将p的next节点设置为n 
            } else if (p.casNext(null, n)) { 
 //6.如果tali节点大于等于1个next节点,将入队节点设为tail节点
             if (hops >= HOPS)  
                casTail(t, n); // 失败了没关系。说明别的线程已经更新
         return true; // 7.添加成功。  
            } else { 
            //8.说明5中设置fail,p有next节点。那么获取next节点
                p = succ(p); //  
            }  
        }  
    }  
}  
 final Node<E> succ(Node<E> p) {  
     Node<E> next = p.getNext();  
     //如果p节点的next节点指向自身,那么返回head节点;否则返回p的next节点。  
     return (p == next) ? head : next;  
 }  
/** 
 * 允许头尾节点的指针滞后,所以当头尾节点离"实际位置"的距离  
 * (按节点)小于HOPS时,不会去更新头尾指针。这里是假设volatile写代价比volatile读高。 
 */  
 private static final int HOPS = 1;  

从上面代码看出,整个入队过程干2件事:一件是定位尾节点,一件是设置成尾节点的next节点。
第一步定位尾节点:因为tail节点不一定是尾节点,有可能是tail的next节点。所以代码开始就判断是否有尾节点。
获取tail节点的next节点需要注意的是p节点等于p的next节点的情况,只有一种可能就是p节点和p的next节点都等于空,表示这个队列刚初始化,正准备添加第一次节点,所以需要返回head节点
第二步设置入队节点为尾节点。p.casNext(null,n)方法用于将入队节点设置为当前队列尾节点的next节点,p如果是null表示p是当前队列的尾节点,如果不为null表示有其他线程更新了尾节点,则需要重新获取当前队列的尾节点。

为什么不让tail一直指向尾节点?

这样做行不行?

public boolean offer(E e) {
       if (e == null)
         throw new NullPointerException();
      Node<e> n = new Node<>(e);
      for (;;) {
         Node<> t = tail;
         if (t.casNext(null, n) && casTail(t, n)) {
            return true;
         }
      }
    }

这样做,逻辑不是非常清楚么。

是这么做有个缺点就是每次都需要使用循环CAS更新tail节点。如果能减少CAS更新tail节点的次数,就能提高入队的效率。

再看下poll方法

public E poll() {
           Node</e><e> h = head;
       // p表示头节点,需要出队的节点
           Node</e><e> p = h;
           for (int hops = 0;; hops++) {
                // 获取p节点的元素
                E item = p.getItem();
                //如果p节点的元素不为空,使用CAS设置p节点引用的元素为null,如果成功则返回p节点的元素。
                if (item != null && p.casItem(item, null)) {
                     if (hops >= HOPS) {
                          //将p节点下一个节点设置成head节点
                          Node</e><e> q = p.getNext();
                          updateHead(h, (q != null) ? q : p);
                     }
                     return item;
                }
                // 如果头节点的元素为空或头节点发生了变化,这说明头节点已经被另外一个线程修改了。那么获取p节点的下一个节点
                Node</e><e> next = succ(p);
                // 如果p的下一个节点也为空,说明这个队列已经空了
                if (next == null) {
              // 更新头节点。
                     updateHead(h, p);
                     break;
                }
                // 如果下一个元素不为空,则将头节点的下一个节点设置成头节点
                p = next;
           }
           return null;
     }