9.並發包非阻塞隊列ConcurrentLinkedQueue


jdk1.7.0_79 

  隊列是一種非常常用的數據結構,一進一出,先進先出。 

  在Java並發包中提供了兩種類型的隊列,非阻塞隊列與阻塞隊列,當然它們都是線程安全的,無需擔心在多線程並發環境所帶來的不可預知的問題。為什么會有非阻塞和阻塞之分呢?這里的非阻塞與阻塞在於有界與否,也就是在初始化時有沒有給它一個默認的容量大小,對於阻塞有界隊列來講,如果隊列滿了的話,則任何線程都會阻塞不能進行入隊操作,反之隊列為空的話,則任何線程都不能進行出隊操作。而對於非阻塞無界隊列來講則不會出現隊列滿或者隊列空的情況。它們倆都保證線程的安全,即不能有一個以上的線程同時對隊列進行入隊或者出隊操作。 

  非阻塞隊列:ConcurrentLinkedQueue 

  阻塞隊列:ArrayBlockingQueueLinkedBlockingQueue、…… 

  本文介紹非阻塞隊列——ConcurentLinkedQueue。 

  首先查看ConcurrentLinkedQueue默認構造函數,觀察它在初始化時做了什么操作。 

//ConcurrentLinkedQueue 
public ConcurrentLinkedQueue() { 
  head = tail = new Node<E>(null); 
}

  可以看到ConcurrentLinkedQueue在其內部有一個頭節點和尾節點,在初始化的時候指向一個節點。 

  對於入隊(插入)操作一共提供了這么2個方法(實際上是一個): 

 

 

 

 

入隊(插入) 

add(e)(其內部調用offer方法 

offer(e)(插入到隊列尾部,當隊列無界將永遠返回true) 

 1 //ConcurrentLinkedQueue#offer
 2 public boolean offer(E e) {
 3     checkNotNull(e);    //入隊元素是否為空,不允許Null值入隊
 4     final Node<E> newNode = new Node<E>(e);    //將入隊元素構造為Node節點
 5     /*tail指向的是隊列尾節點,但有時tail.next才是真正指向的尾節點*/
 6     for (Node<E> t = tail, p = t;;) {
 7         Node<E> q = p.next;
 8         if (q == null) {    //此時p指向的就是隊列真正的尾節點
 9             if(p.casNext(null, newNode)) {    //cas算法,p.next = newNode
10                 if (p != tail)     //將tail指向隊列尾節點
11                     casTail(t, newNode);
12                 return true;
13        }
14     }
15     else if (p == q) 
16         p = (t != (t = tail)) ? t : head;
17     else
18         p = (p != t && t != (t = tail)) t : q;
19   }
20 }

  offer入隊過程如下圖所示:
  ① 隊列中沒有元素,第一次入隊操作:
    進入循環體:
    t = tail;
    p = tail;
    q = p.next = null;

    判斷尾節點的引用p是否指向的是尾節點(if(q == null))->是:
      CAS算法將入隊節點設置成尾節點的next節點(p.casNext(null, newNode))
    判斷tail尾節點指針的引用p是否大於等於1個next節點(if (p != t))->否
    返回true

  ② 隊列中有元素,進行入隊操作:

    1) 第一次循環:
    t = tail;
    p = tail;
    q = p.next = Node1;

    判斷tail尾節點指針的引用p是否指向的是尾節點(if(q == null))->否
    判斷tail尾節點指針的引用p是否指向的是尾節點(else if (p == q))->否
    將tail尾節點指針的引用p向后移動(p = (p != t && t != (t = tail)) ? t : q;)->p = Node1

    2) 第二次循環:
    t = tail;
    p = Node1;
    q = p.next = null;

    判斷tail尾節點指針的引用p是否指向真正的尾節點(if(q == null))->是:
      CAS算法將入隊節點設置成尾節點的next節點(p.casNext(null, newNode))
    判斷tail尾節點指針的引用p是否大於等於1個next節點(if (p != t))->是:
      更新tail節點(casTail(t, nextNode))
    返回true

  入隊的操作都是由CAS算法完成,顯然是為了保證其安全性。整個入隊過程首先要定位出尾節點,其次使用CAS算法將入隊節點設置成尾節點的next節點。整個入隊過程首先要定位隊列的尾節點,如果將tail節點一直指向尾節點豈不是更好嗎?每次即tail->next = newNode;tail = newNode;這樣在單線程環境來確實沒問題,但是,在多線程並發環境下就不得不要考慮線程安全,每次更新tail節點意味着每次都要使用CAS更新tail節點,這樣入隊效率必然降低,所以ConcurrentLinkedQueue的tail節點並不總是指向隊列尾節點的原因就是減少更新tail節點的次數,提高入隊效率。
  對於出隊(刪除)操作一共提供了這么1個方法:

 1 //ConcurrentLinkecQueue#poll
 2 public E poll() {
 3     restartFromHead:
 4     for (;;) {
 5         for (Node<E> h = head, p = h, q;;) {
 6             E item = p.item;
 7             if (item != null && p.casItem(item, null)) {
 8                 if (p != h)
 9                     updateHead(h, ((q = p.next) != null) ? q : p);
10                 return item;
11        }  
12         else if ((q = p.next) == null) {
13              updateHead(h, p);
14            return null;
15        }
16         else if (p == q)
17            continue restartFromHead;
18         else
19             p = q;
20     }
21   }
22 }

  以上面隊列中有兩個元素為例:(注意,初始時,head指向的是空節點)

  出隊(刪除):
  1) 第一次循環:
    h = head;
    p = head;
    q = null;
    item = p.item = null;

 

    判斷head節點指針的引用是否不是空節點(if (item != null))->否,即是空節點
    判斷(暫略)
    判斷(暫略)
    將head節點指針的引用p向后移動(p = q)

  2) 第二次循環:
    h = head;
    p = q = Node1;
    q = Node1;
    item = p.item = Node1.item;

    判斷head節點指針的引用p是否不是空節點(if (item != null))->是,即不是空節點:
      判斷head節點指針與p是否指向同一節點(if (p != h))->否:
        更新頭節點(updateHead(h, ((q = p.next) != null) ? q : p))
        返回item

  實際上繼續出隊會發現,出隊和入隊類似,不會每次出隊都會更新head節點,原理也和tail一樣。
  對於ConcurrentLinkedQueue#size方法將會遍歷整個隊列,可想它的效率並不高,如果一定需要調用它的size方法,特別是for循環時,我建議一下寫法:

for (int i = 0, int size = concurrentLinkedQueue.size(); i < size;i++)

  因為這能保證不用每次循環都調用一次size方法遍歷一遍隊列。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM