jdk1.7.0_79
隊列是一種非常常用的數據結構,一進一出,先進先出。
在Java並發包中提供了兩種類型的隊列,非阻塞隊列與阻塞隊列,當然它們都是線程安全的,無需擔心在多線程並發環境所帶來的不可預知的問題。為什么會有非阻塞和阻塞之分呢?這里的非阻塞與阻塞在於有界與否,也就是在初始化時有沒有給它一個默認的容量大小,對於阻塞有界隊列來講,如果隊列滿了的話,則任何線程都會阻塞不能進行入隊操作,反之隊列為空的話,則任何線程都不能進行出隊操作。而對於非阻塞無界隊列來講則不會出現隊列滿或者隊列空的情況。它們倆都保證線程的安全性,即不能有一個以上的線程同時對隊列進行入隊或者出隊操作。
非阻塞隊列:ConcurrentLinkedQueue
阻塞隊列:ArrayBlockingQueue、LinkedBlockingQueue、……
本文介紹非阻塞隊列——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方法遍歷一遍隊列。