一、 前言
常用的併發隊列有阻塞隊列和非阻塞隊列,前者使用鎖實現,後者則使用CAS非阻塞算法實現,使用非阻塞隊列一般性能比較好,下面就看看常用的非阻塞ConcurrentLinkedQueue是如何使用CAS實現的。ConcurrentLinkedQueue是無界隊列。
對於使用鎖實現阻塞隊列的也分兩種情況,一種是使用一把鎖,例如ArrayBlockingQueue;還有一種是LinkedBlockingQueue採用了鎖分離的技術,也就是將put和take對應兩把鎖。
二、 ConcurrentLinkedQueue類圖結構
如圖ConcurrentLinkedQueue中有兩個volatile類型的Node節點分別用來存在列表的首尾節點,其中head節點存放鏈表第一個item爲null的節點,tail則並不是總指向最後一個節點。Node節點內部則維護一個變量item用來存放節點的值,next用來存放下一個節點,從而鏈接爲一個單向無界列表。
1 2 3 | public ConcurrentLinkedQueue() { head = tail = new Node<E>( null ); } |
如上代碼初始化時候會構建一個item爲NULL的空節點作爲鏈表的首尾節點。
三、offer操作
offer操作是在鏈表末尾添加一個元素,下面看看實現原理。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | public boolean offer(E e) { //e爲null則拋出空指針異常 checkNotNull(e); //構造Node節點構造函數內部調用unsafe.putObject,後面統一講 final Node<E> newNode = new Node<E>(e); //從尾節點插入 for (Node<E> t = tail, p = t;;) { Node<E> q = p.next; //如果q=null說明p是尾節點則插入 if (q == null ) { //cas插入(1) if (p.casNext( null , newNode)) { //cas成功說明新增節點已經被放入鏈表,然後設置當前尾節點(包含head,1,3,5.。。個節點爲尾節點) 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 } else if (p == q) //(2) //多線程操作時候,由於poll時候會把老的head變爲自引用,然後head的next變爲新head,所以這裏需要 //重新找新的head,因爲新的head後面的節點纔是激活的節點 p = (t != (t = tail)) ? t : head; else // 尋找尾節點(3) p = (p != t && t != (t = tail)) ? t : q; } } |
從構造函數知道一開始有個item爲null的哨兵節點,並且head和tail都是指向這個節點,然後當一個線程調用offer時候首先
如圖首先查找尾節點,q==null,p就是尾節點,所以執行p.casNext通過cas設置p的next爲新增節點,這時候p==t所以不重新設置尾節點爲當前新節點。由於多線程可以調用offer方法,所以可能兩個線程同時執行到了(1)進行cas,那麼只有一個會成功(假如線程1成功了),成功後的鏈表爲:
失敗的線程會循環一次這時候指針爲:
這時候會執行(3)所以p=q,然後在循環後指針位置爲:
所以沒有其他線程干擾的情況下會執行(1)執行cas把新增節點插入到尾部,沒有干擾的情況下線程2 cas會成功,然後去更新尾節點tail,由於p!=t所以更新。這時候鏈表和指針爲:
假如線程2cas時候線程3也在執行,那麼線程3會失敗,循環一次後,線程3的節點狀態爲:
這時候p!=t ;並且t的原始值爲told,t的新值爲tnew ,所以told!=tnew,所以 p=tnew=tail;
然後在循環一下後節點狀態:
q==null所以執行(1)。
現在就差p==q這個分支還沒有走,這個要在執行poll操作後纔會出現這個情況。poll後會存在下面的狀態
這個時候添加元素時候指針分佈爲:
所以會執行(2)分支 結果 p=head
然後循環,循環後指針分佈:
所以執行(1),然後p!=t所以設置tail節點。現在分佈圖:
自引用的節點會被垃圾回收掉。
四、 add操作
add操作是在鏈表末尾添加一個元素,下面看看實現原理。
其實內部調用的還是offer
1 2 3 | public boolean add(E e) { return offer(e); } |
五、poll操作
poll操作是在鏈表頭部獲取並且移除一個元素,下面看看實現原理。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 | public E poll() { restartFromHead: //死循環 for (;;) { //死循環 for (Node<E> h = head, p = h, q;;) { //保存當前節點值 E item = p.item; //當前節點有值則cas變爲null(1) if (item != null && p.casItem(item, null )) { //cas成功標誌當前節點以及從鏈表中移除 if (p != h) // 類似tail間隔2設置一次頭節點(2) updateHead(h, ((q = p.next) != null ) ? q : p); return item; } //當前隊列爲空則返回null(3) else if ((q = p.next) == null ) { updateHead(h, p); return null ; } //自引用了,則重新找新的隊列頭節點(4) else if (p == q) continue restartFromHead; else //(5) p = q; } } } final void updateHead(Node<E> h, Node<E> p) { if (h != p && casHead(h, p)) h.lazySetNext(h); } |
當隊列爲空時候:
可知執行(3)這時候有兩種情況,第一沒有其他線程添加元素時候(3)結果爲true然後因爲h!=p爲false所以直接返回null。第二在執行q=p.next前,其他線程已經添加了一個元素到隊列,這時候(3)返回false,然後執行(5)p=q,然後循環後節點分佈:
這時候執行(1)分支,進行cas把當前節點值值爲null,同時只有一個線程會成功,cas成功 標示該節點從隊列中移除了,然後p!=h,調用updateHead方法,參數爲h,p;h!=p所以把p變爲當前鏈表head節點,然後h節點的next指向自己。現在狀態爲:
cas失敗 後 會再次循環,這時候分佈圖爲:
這時候執行(3)返回null.
現在還有個分支(4)沒有執行過,那麼什麼時候會執行那?
這時候執行(1)分支,進行cas把當前節點值值爲null,同時只有一個線程A會成功,cas成功 標示該節點從隊列中移除了,然後p!=h,調用updateHead方法,假如執行updateHead前另外一個線程B開始poll這時候它p指向爲原來的head節點,然後當前線程A執行updateHead這時候B線程鏈表狀態爲:
所以會執行(4)重新跳到外層循環,獲取當前head,現在狀態爲:
六、peek操作
peek操作是獲取鏈表頭部一個元素(只讀取不移除),下面看看實現原理。
代碼與poll類似,只是少了castItem.並且peek操作會改變head指向,offer後head指向哨兵節點,第一次peek後head會指向第一個真的節點元素。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | 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; } } } |
七、size操作
獲取當前隊列元素個數,在併發環境下不是很有用,因爲使用CAS沒有加鎖所以從調用size函數到返回結果期間有可能增刪元素,導致統計的元素個數不精確。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 | public int size() { int count = 0 ; for (Node<E> p = first(); p != null ; p = succ(p)) if (p.item != null ) // 最大返回Integer.MAX_VALUE if (++count == Integer.MAX_VALUE) break ; return count; } //獲取第一個隊列元素(哨兵元素不算),沒有則爲null 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; } } } //獲取當前節點的next元素,如果是自引入節點則返回真正頭節點 final Node<E> succ(Node<E> p) { Node<E> next = p.next; return (p == next) ? head : next; } |
八、remove操作
如果隊列裏面存在該元素則刪除給元素,如果存在多個則刪除第一個,並返回true,否者返回false
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | public boolean remove(Object o) { //查找元素爲空,直接返回false if (o == null ) return false ; Node<E> pred = null ; for (Node<E> p = first(); p != null ; p = succ(p)) { E item = p.item; //相等則使用cas值null,同時一個線程成功,失敗的線程循環查找隊列中其他元素是否有匹配的。 if (item != null && o.equals(item) && p.casItem(item, null )) { //獲取next元素 Node<E> next = succ(p); //如果有前驅節點,並且next不爲空則鏈接前驅節點到next, if (pred != null && next != null ) pred.casNext(p, next); return true ; } pred = p; } return false ; } |
九、contains操作
判斷隊列裏面是否含有指定對象,由於是遍歷整個隊列,所以類似size 不是那麼精確,有可能調用該方法時候元素還在隊列裏面,但是遍歷過程中才把該元素刪除了,那麼就會返回false.
1 2 3 4 5 6 7 8 9 | public boolean contains(Object o) { if (o == null ) return false ; for (Node<E> p = first(); p != null ; p = succ(p)) { E item = p.item; if (item != null && o.equals(item)) return true ; } return false ; } |
十、開源框架中使用
Tomcat中NioEndPoint中的每個poller裏面就維護一個ConcurrentLinkedQueue<Runnable>用來作爲緩衝存放任務。
10.1 Acceptor線程
accept線程作用是接受客戶端發來的連接請求並放入到事件隊列。
看下代碼:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 | protected class Acceptor extends AbstractEndpoint.Acceptor { @Override public void run() { int errorDelay = 0 ; // 一直循環直到接收到shutdown命令 while (running) { ... if (!running) { break ; } state = AcceptorState.RUNNING; try { //如果達到max connections個請求則等待 countUpOrAwaitConnection(); SocketChannel socket = null ; try { // 從TCP緩存獲取一個完成三次握手的套接字,沒有則阻塞 // socket socket = serverSock.accept(); } catch (IOException ioe) { ... } // Successful accept, reset the error delay errorDelay = 0 ; if (running && !paused) { if (!setSocketOptions(socket)) { countDownConnection(); closeSocket(socket); } } else { countDownConnection(); closeSocket(socket); } .... } catch (SocketTimeoutException sx) { // Ignore: Normal condition .... } state = AcceptorState.ENDED; } } protected boolean setSocketOptions(SocketChannel socket) { // Process the connection try { //disable blocking, APR style, we are gonna be polling it ... getPoller0().register(channel); } catch (Throwable t) { ... return false ; } return true ; } public void register( final NioChannel socket) { ... addEvent(r); } public void addEvent(Runnable event) { events.offer(event); ... } |
10.2 Poll線程
poll線程作用是從事件隊列裏面獲取事件把鏈接套接字加入selector,並且監聽socket事件進行處理。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 | public void run() { while ( true ) { try { ... if (close) { ... } else { hasEvents = events(); } try { ... } catch ( NullPointerException x ) {... } Iterator<SelectionKey> iterator = keyCount > 0 ? selector.selectedKeys().iterator() : null ; // 遍歷所有註冊的channel對感興趣的事件處理 while (iterator != null && iterator.hasNext()) { SelectionKey sk = iterator.next(); KeyAttachment attachment = (KeyAttachment)sk.attachment(); if (attachment == null ) { iterator.remove(); } else { attachment.access(); iterator.remove(); processKey(sk, attachment); } } //while //process timeouts timeout(keyCount,hasEvents); if ( oomParachute > 0 && oomParachuteData == null ) checkParachute(); } catch (OutOfMemoryError oom) { ... } } //while synchronized ( this ) { this .notifyAll(); } stopLatch.countDown(); } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 | public boolean events() { boolean result = false ; //從隊列獲取任務並執行 Runnable r = null ; while ( (r = events.poll()) != null ) { result = true ; try { r.run(); if ( r instanceof PollerEvent ) { ((PollerEvent)r).reset(); eventCache.offer((PollerEvent)r); } } catch ( Throwable x ) { log.error( "" ,x); } } return result; } //如配置線程池則請求交給線程池處理。 public boolean processSocket(NioChannel socket, SocketStatus status, boolean dispatch) { try { KeyAttachment attachment = (KeyAttachment)socket.getAttachment(); if (attachment == null ) { return false ; } attachment.setCometNotify( false ); //will get reset upon next reg SocketProcessor sc = processorCache.poll(); if ( sc == null ) sc = new SocketProcessor(socket,status); else sc.reset(socket,status); if ( dispatch && getExecutor()!= null ) getExecutor().execute(sc); else sc.run(); } catch (RejectedExecutionException rx) { ... } return true ; } |
十一、有意思的問題
10.1 一個判斷的執行結果分析
offer中有個 判斷 t != (t = tail)假如 t=node1;tail=node2;並且node1!=node2那麼這個判斷是true還是false那,答案是true,這個判斷是看當前t是不是和tail相等,相等則返回true否者爲false,但是無論結果是啥執行後t的值都是tail。
下面從字節碼來分析下爲啥?
- 一個例子
1 2 3 4 5 6 7 8 9 | public static void main(String[] args) { int t = 2 ; int tail = 3 ; System.out.println(t != (t = tail)); } |
結果爲:true;
- 字節碼文件:
字節碼命令介紹可參考: http://blog.csdn.net/web_code/article/details/12164733
一開始棧爲空
- 第0行指令作用是把值2入棧棧頂元素爲2
- 第1行指令作用是將棧頂int類型值保存到局部變量t中。
- 第2行指令作用是把值3入棧棧頂元素爲3
- 第3行指令作用是將棧頂int類型值保存到局部變量tail中。
- 第4調用打印命令
- 第7行指令作用是把變量t中的值入棧
- 第8行指令作用是把變量tail中的值入棧
- 現在棧裏面元素爲3,2並且3位棧頂
- 第9行指令作用是當前棧頂元素入棧,所以現在棧內容3,3,2
- 第10行指令作用是把棧頂元素存放到t,現在棧內容3,2
- 第11行指令作用是判斷棧頂兩個元素值,相等則跳轉 18。由於現在棧頂嚴肅爲3,2不相等所以返回true.
- 第14行指令作用是把1入棧。
然後回頭分析下!=是雙目運算符,應該是首先把左邊的操作數入棧,然後在去計算了右側操作數。
10.2 Node的構造函數
另外對於每個節點Node在構造時候使用UnSafe.putObject設置item替代了直接對volatile的賦值,這個是爲了性能考慮?爲啥不直接賦值那,看看類註解怎麼說:
1 2 3 | Node(E item) { UNSAFE.putObject( this , itemOffset, item); } |
When constructing a Node (before enqueuing it) we avoid paying for a volatile write to item by using Unsafe.putObject instead of a normal write. This allows the cost of enqueue to be”one-and-a-half”
CASes.
也就是說當構造Node節點時候(這時候節點還沒有放入隊列鏈表)爲了避免正常的寫volatile變量的開銷 使用了Unsafe.putObject代替。這使元素進隊列僅僅花費1.5個cas操作的耗時。這個是說使用Unsafe.putObject比直接給volatile變量賦值更高效?目前還沒有查到相關資料。
十二、總結
ConcurrentLinkedQueue使用CAS非阻塞算法實現使用CAS解決了當前節點與next節點之間的安全鏈接和對當前節點值的賦值。由於使用CAS沒有使用鎖,所以獲取size的時候有可能進行offer,poll或者remove操作,導致獲取的元素個數不精確,所以在併發情況下size函數不是很有用。另外第一次peek或者first時候會把head指向第一個真正的隊列元素。
下面總結下如何實現線程安全的,可知入隊出隊函數都是操作volatile變量:head,tail。所以要保證隊列線程安全只需要保證對這兩個Node操作的可見性和原子性,由於volatile本身保證可見性,所以只需要看下多線程下如果保證對着兩個變量操作的原子性。
對於offer操作是在tail後面添加元素,也就是調用tail.casNext方法,而這個方法是使用的CAS操作,只有一個線程會成功,然後失敗的線程會循環一下,重新獲取tail,然後執行casNext方法。對於poll也是這樣的。