前言
SynchronousQueue是一個比較特別的佇列,由於線上程池方面有所應用,為了更好的理解執行緒池的實現原理,筆者花了些時間學習了一下該佇列原始碼(JDK1.8),此佇列原始碼中充斥著大量的CAS語句,理解起來是有些難度的,為了方便日後回顧,本篇文章會以簡潔的圖形化方式展示該佇列底層的實現原理。
SynchronousQueue簡單使用
經典的生產者-消費者模式,操作流程是這樣的:
有多個生產者,可以併發生產產品,把產品置入佇列中,如果佇列滿了,生產者就會阻塞;
有多個消費者,併發從佇列中獲取產品,如果佇列空了,消費者就會阻塞;
如下面的示意圖所示:
SynchronousQueue 也是一個佇列來的,但它的特別之處在於它內部沒有容器,一個生產執行緒,當它生產產品(即put的時候),如果當前沒有人想要消費產品(即當前沒有執行緒執行take),此生產執行緒必須阻塞,等待一個消費執行緒呼叫take操作,take操作將會喚醒該生產執行緒,同時消費執行緒會獲取生產執行緒的產品(即資料傳遞),這樣的一個過程稱為一次配對過程(當然也可以先take後put,原理是一樣的)。
用一個圖來形象的展示同步佇列(SynchronousQueue):
我們用一個簡單的程式碼來驗證一下,如下所示:
package com.concurrent; import java.util.concurrent.SynchronousQueue; public class SynchronousQueueDemo { public static void main(String[] args) throws InterruptedException { final SynchronousQueue<Integer> queue = new SynchronousQueue<Integer>(); Thread putThread = new Thread(new Runnable() { @Override public void run() { System.out.println("put thread start"); try { queue.put(1); } catch (InterruptedException e) { } System.out.println("put thread end"); } }); Thread takeThread = new Thread(new Runnable() { @Override public void run() { System.out.println("take thread start"); try { System.out.println("take from putThread: " + queue.take()); } catch (InterruptedException e) { } System.out.println("take thread end"); } }); putThread.start(); Thread.sleep(1000); takeThread.start(); } }
一種輸出結果如下:
put thread start take thread start take from putThread: 1 put thread end take thread end
從結果可以看出,put執行緒執行queue.put(1) 後就被阻塞了,只有take執行緒進行了消費,put執行緒才可以返回。可以認為這是一種執行緒與執行緒間一對一傳遞訊息的模型。
SynchronousQueue實現原理
不像ArrayBlockingQueue、LinkedBlockingDeque之類的阻塞佇列依賴AQS實現併發操作,SynchronousQueue直接使用CAS實現執行緒的安全訪問。由於原始碼中充斥著大量的CAS程式碼,不易於理解,所以按照筆者的風格,接下來會使用簡單的示例來描述背後的實現模型。
佇列的實現策略通常分為公平模式和非公平模式,接下來將分別進行說明。
公平模式下的模型:
公平模式下,底層實現使用的是TransferQueue這個內部佇列,它有一個head和tail指標,用於指向當前正在等待匹配的執行緒節點。
初始化時,TransferQueue的狀態如下:
接著我們進行一些操作:
1、執行緒put1執行 put(1)操作,由於當前沒有配對的消費執行緒,所以put1執行緒入佇列,自旋一小會後睡眠等待,這時佇列狀態如下:
2、接著,執行緒put2執行了put(2)操作,跟前面一樣,put2執行緒入佇列,自旋一小會後睡眠等待,這時佇列狀態如下:
3、這時候,來了一個執行緒take1,執行了 take操作,由於tail指向put2執行緒,put2執行緒跟take1執行緒配對了(一put一take),這時take1執行緒不需要入隊,但是請注意了,這時候,要喚醒的執行緒並不是put2,而是put1。為何? 大家應該知道我們現在講的是公平策略,所謂公平就是誰先入隊了,誰就優先被喚醒,我們的例子明顯是put1應該優先被喚醒。至於讀者可能會有一個疑問,明明是take1執行緒跟put2執行緒匹配上了,結果是put1執行緒被喚醒消費,怎麼確保take1執行緒一定可以和次首節點(head.next)也是匹配的呢?其實大家可以拿個紙畫一畫,就會發現真的就是這樣的。
公平策略總結下來就是:隊尾匹配隊頭出隊。
執行後put1執行緒被喚醒,take1執行緒的 take()方法返回了1(put1執行緒的資料),這樣就實現了執行緒間的一對一通訊,這時候內部狀態如下:
4、最後,再來一個執行緒take2,執行take操作,這時候只有put2執行緒在等候,而且兩個執行緒匹配上了,執行緒put2被喚醒,
take2執行緒take操作返回了2(執行緒put2的資料),這時候佇列又回到了起點,如下所示:
以上便是公平模式下,SynchronousQueue的實現模型。總結下來就是:隊尾匹配隊頭出隊,先進先出,體現公平原則。
非公平模式下的模型(預設):
我們還是使用跟公平模式下一樣的操作流程,對比兩種策略下有何不同。非公平模式底層的實現使用的是TransferStack,
一個棧,實現中用head指標指向棧頂,接著我們看看它的實現模型:
1、執行緒put1執行 put(1)操作,由於當前沒有配對的消費執行緒,所以put1執行緒入棧,自旋一小會後睡眠等待,這時棧狀態如下:
2、接著,執行緒put2再次執行了put(2)操作,跟前面一樣,put2執行緒入棧,自旋一小會後睡眠等待,這時棧狀態如下:
3、這時候,來了一個執行緒take1,執行了take操作,這時候發現棧頂為put2執行緒,匹配成功,但是實現會先把take1執行緒入棧,然後take1執行緒迴圈執行匹配put2執行緒邏輯,一旦發現沒有併發衝突,就會把棧頂指標直接指向 put1執行緒
4、最後,再來一個執行緒take2,執行take操作,這跟步驟3的邏輯基本是一致的,take2執行緒入棧,然後在迴圈中匹配put1執行緒,最終全部匹配完畢,棧變為空,恢復初始狀態,如下圖所示:
可以從上面流程看出,雖然put1執行緒先入棧了,但是卻是後匹配,這就是非公平的由來。
總結
SynchronousQueue由於其獨有的執行緒一一配對通訊機制,在大部分平常開發中,可能都不太會用到,但執行緒池技術中會有所使用,由於內部沒有使用AQS,而是直接使用CAS,所以程式碼理解起來會比較困難,但這並不妨礙我們理解底層的實現模型,在理解了模型的基礎上,有興趣的話再查閱原始碼,就會有方向感,看起來也會比較容易,希望本文有所借鑑意義。