1 Java中的阻塞隊列
1.1 簡介
一種支持兩個附加操作的隊列,是一系列阻塞隊列類的接口
當存取條件不滿足時,阻塞在操作處
-
隊列滿時,阻塞存儲元素的線程,直到隊列可用
-
隊列空時,獲取元素的線程會等待隊列非空
阻塞隊列常用於生產者/消費者場景,生產者是向隊列裏存元素的線程,消費者是從隊列裏取元素的線程.阻塞隊列就是生產者存儲元素、消費者獲取元素的容器
BlockingQueue繼承體系
阻塞隊列不可用時,兩個附加操作提供了4種處理方式
-
拋出異常
-
當隊列滿時,如果再往隊列裏插入元素,會拋出IllegalStateException("Queuefull")異常
-
當隊列空時,從隊列裏獲取元素會拋出NoSuchElementException異常
-
-
返回特殊值
-
當往隊列插入元素時,會返回元素是否插入成功,成功則返回true
-
若是移除方法,則是從隊列裏取出一個元素,若沒有則返回null
-
-
一直阻塞
-
當阻塞隊列滿時,如果生產者線程往隊列裏put元素,隊列會一直阻塞生產者線程,直到隊列有可用空間或響應中斷退出
-
當隊列空時,若消費者線程從隊列裏take元素,隊列會阻塞住消費者線程,直到隊列非空
-
-
超時退出
-
當阻塞隊列滿時,若生產者線程往隊列裏插入元素,隊列會阻塞生產者線程
一段時間,若超過指定的時間,生產者線程就會退出
-
若是無界阻塞隊列,隊列不會出現滿的情況,所以使用put或offer方法永遠不會被阻塞,使用offer方法時,永遠返回true
BlockingQueue 不接受 null 元素,拋 NullPointerException
null 被用作指示 poll 操作失敗的警戒值(無法通過編譯)
BlockingQueue 實現主要用於生產者/使用者隊列,但它另外還支持 Collection 接口。
因此,舉例來說,使用 remove(x) 從隊列中移除任意一個元素是有可能的。
然而,這種操作通常表現並不高效,只能有計劃地偶爾使用,比如在取消排隊信息時。
BlockingQueue 的實現是線程安全的
所有排隊方法都可使用內置鎖或其他形式的併發控制來自動達到它們的目的
然而,大量的Collection 操作(addAll、containsAll、retainAll 和 removeAll)沒有必要自動執行,除非在實現中特別說明
因此,舉例來說,在只添加 c 中的一些元素後,addAll(c) 有可能失敗(拋出一個異常)
BlockingQueue 實質上不支持使用任何一種“close”或“shutdown”操作來指示不再添加任何項
這種功能的需求和使用有依賴於實現的傾向
例如,一種常用的策略是:對於生產者,插入特殊的 end-of-stream 或 poison 對象,並根據使用者獲取這些對象的時間來對它們進行解釋
2 生產者和消費者例子
在介紹具體的阻塞類之前,先來看看阻塞隊列最常應用的場景,即生產者和消費者例子
一般而言,有n個生產者,各自生產產品,並放入隊列
同時有m個消費者,各自從隊列中取出產品消費
當隊列已滿時(隊列可以在初始化時設置Capacity容量),生產者會在放入隊列時阻塞;當隊列空時,消費者會在取出產品時阻塞。代碼如下:
public class BlockingQueueExam { public static void main(String[] args) throws InterruptedException { BlockingQueue<String> blockingQueue = new LinkedBlockingQueue<>(3); ExecutorService service = Executors.newCachedThreadPool(); for (int i = 0; i < 5; i++) { service.submit(new Producer("Producer" + i, blockingQueue)); } for (int i = 0; i < 5; i++) { service.submit(new Consumer("Consumer" + i, blockingQueue)); } service.shutdown(); } } class Producer implements Runnable { private final String name; private final BlockingQueue<String> blockingQueue; private static Random rand = new Random(47); private static AtomicInteger productID = new AtomicInteger(0); Producer(String name, BlockingQueue<String> blockingQueue) { this.name = name; this.blockingQueue = blockingQueue; } @Override public void run() { try { for (int i = 0; i < 10; i++) { SECONDS.sleep(rand.nextInt(5)); String str = "Product" + productID.getAndIncrement(); blockingQueue.add(str); //注意,這裏得到的size()有可能是錯誤的 System.out.println(name + " product " + str + ", queue size = " + blockingQueue.size()); } System.out.println(name + " is over"); } catch (InterruptedException e) { e.printStackTrace(); } } } class Consumer implements Runnable { private final String name; private final BlockingQueue<String> blockingQueue; private static Random rand = new Random(47); Consumer(String name, BlockingQueue<String> blockingQueue) { this.name = name; this.blockingQueue = blockingQueue; } @Override public void run() { try { for (int i = 0; i < 10; i++) { SECONDS.sleep(rand.nextInt(5)); String str = blockingQueue.take(); //注意,這裏得到的size()有可能是錯誤的 System.out.println(name + " consume " + str + ", queue size = " + blockingQueue.size()); } System.out.println(name + " is over"); } catch (InterruptedException e) { e.printStackTrace(); } } }
以上代碼中的阻塞隊列是LinkedBlockingQueue,初始化容量爲3
生產者5個,每個生產者間隔隨機時間後生產一個產品put放入隊列,每個生產者生產10個產品
消費者也是5個,每個消費者間隔隨機時間後take取出一個產品進行消費,每個消費者消費10個產品
可以看到,當隊列滿時,所有生產者被阻塞
當隊列空時,所有消費者被阻塞
代碼中還用到了AtomicInteger原子整數,用來確保產品的編號不會混亂
2 Java裏的阻塞隊列
BlockingQueue的實現類
至JDK8,Java提供了7個阻塞隊列
-
ArrayBlockingQueue:數組結構組成的有界阻塞隊列
-
LinkedBlockingQueue:鏈表結構組成的有界(默認MAX_VALUE容量)阻塞隊列
-
PriorityBlockingQueue:支持優先級調度的無界阻塞隊列,排序基於compareTo或Comparator完成
-
DelayQueue:支持延遲獲取元素,在OS調用中較多或者應用於某些條件變量達到要求後需要做的事情
-
SynchronousQueue:一個不存儲元素的阻塞隊列。
-
LinkedTransferQueue:鏈表結構的TransferQueue,無界阻塞隊列
-
LinkedBlockingDeque:鏈表結構的雙向阻塞隊列
2.1 LinkedBlockingQueue和ArrayBlockingQueue
基於數組的阻塞隊列實現,在ArrayBlockingQueue
內部,維護了一個定長數組,以便緩存隊列中的數據對象,這是一個常用的阻塞隊列,除了一個定長數組外,ArrayBlockingQueue內部還保存着兩個整形變量,分別標識着隊列的頭部和尾部在數組中的位置。
ArrayBlockingQueue在生產者放入數據和消費者獲取數據,都是共用同一個鎖對象,由此也意味着兩者無法真正並行運行,這點尤其不同於LinkedBlockingQueue;按照實現原理來分析,ArrayBlockingQueue完全可以採用分離鎖,從而實現生產者和消費者操作的完全並行運行。Doug Lea之所以沒這樣去做,也許是因爲ArrayBlockingQueue的數據寫入和獲取操作已經足夠輕巧,以至於引入獨立的鎖機制,除了給代碼帶來額外的複雜性外,其在性能上完全佔不到任何便宜。 ArrayBlockingQueue和LinkedBlockingQueue間還有一個明顯的不同之處在於,前者在插入或刪除元素時不會產生或銷燬任何額外的對象實例,而後者則會生成一個額外的Node對象。這在長時間內需要高效併發地處理大批量數據的系統中,其對於GC的影響還是存在一定的區別。而在創建ArrayBlockingQueue時,我們還可以控制對象的內部鎖是否採用公平鎖,默認採用非公平鎖。
都是FIFO隊列
正如其他Java集合一樣,鏈表形式的隊列,其存取效率要比數組形式的隊列高
但是在一些併發程序中,數組形式的隊列由於具有一定的可預測性,因此可以在某些場景中獲得更好的效率
另一個不同點在於,ArrayBlockingQueue支持“公平”策略
若在構造函數中指定了“公平”策略爲true,可以有效避免一些線程被“餓死”,公平性通常會降低吞吐量,但也減少了可變性和避免了“不平衡性"BlockingQueue queue = new ArrayBlockingQueue<>(3, true);
總體而言,LinkedBlockingQueue是阻塞隊列的最經典實現,在不需要“公平”策略時,基本上使用它就夠了
所謂公平訪問隊列是指阻塞的線程,可以按照阻塞的先後順序訪問隊列,即先阻塞的線程先訪問隊列
非公平性是對先等待的線程是非公平的,當隊列有可用空間時,阻塞的線程都可以爭奪訪問隊列的資格,有可能先阻塞的線程最後才訪問隊列
爲保證公平性,通常會降低吞吐量.我們可以使用以下代碼創建一個公平的阻塞隊列
ArrayBlockingQueue fairQueue = new ArrayBlockingQueue(1000,true);
訪問者的公平性是使用可重入鎖實現的
2.2 SynchronousQueue同步隊列
比較特殊的阻塞隊列,它具有以下幾個特點:
-
一個插入方法的線程必須等待另一個線程調用取出
-
隊列沒有容量Capacity(或者說容量爲0),事實上隊列中並不存儲元素,它只是提供兩個線程進行信息交換的場所
-
由於以上原因,隊列在很多場合表現的像一個空隊列。不能對元素進行迭代,不能peek元素,poll會返回null
-
隊列中不允許存入null元素
-
SynchronousQueue如同ArrayedBlockingQueue一樣,支持“公平”策略
下面是一個例子,5個Producer產生產品,存入隊列
5個Consumer從隊列中取出產品,進行消費。
public class SynchronizeQueueExam { public static void main(String[] args) { SynchronousQueue<String> queue = new SynchronousQueue<>(false); ExecutorService service = Executors.newCachedThreadPool(); for (int i = 0; i < 5; i++) { service.submit(new Producer(queue, "Producer" + i)); } for (int i = 0; i < 5; i++) { service.submit(new Consumer(queue, "Consumer" + i)); } service.shutdown(); } static class Producer implements Runnable { private final SynchronousQueue<String> queue; private final String name; private static Random rand = new Random(47); private static AtomicInteger productID = new AtomicInteger(0); Producer(SynchronousQueue<String> queue, String name) { this.queue = queue; this.name = name; } @Override public void run() { try { for (int i = 0; i < 5; i++) { TimeUnit.SECONDS.sleep(rand.nextInt(5)); String str = "Product" + productID.incrementAndGet(); queue.put(str); System.out.println(name + " put " + str); } System.out.println(name + " is over."); } catch (InterruptedException e) { e.printStackTrace(); } } } static class Consumer implements Runnable { private final SynchronousQueue<String> queue; private final String name; Consumer(SynchronousQueue<String> queue, String name) { this.queue = queue; this.name = name; } @Override public void run() { try { for (int i = 0; i < 5; i++) { String str = queue.take(); System.out.println(name + " take " + str); } System.out.println(name + " is over."); } catch (InterruptedException e) { e.printStackTrace(); } } } }
2.3 PriorityBlockingQueue優先級阻塞隊列
-
隊列中的元素總是按照“自然順序”排序,或者根據構造函數中給定的Comparator進行排序
-
隊列中不允許存在null,也不允許存在不能排序的元素
-
對於排序值相同的元素,其序列是不保證的,當然你可以自己擴展這個功能
-
隊列容量是沒有上限的,但是如果插入的元素超過負載,有可能會引起OOM
-
使用迭代子iterator()對隊列進行輪詢,其順序不能保證
-
具有BlockingQueue的put和take方法,但是由於隊列容量沒有上線,所以put方法是不會被阻塞的,但是take方法是會被阻塞的
-
可以給定初始容量,這個容量會按照一定的算法自動擴充
下面是一個PriorityBlockingQueue的例子,例子中定義了一個按照字符串倒序排列的隊列
5個生產者不斷產生隨機字符串放入隊列
5個消費者不斷從隊列中取出隨機字符串
同一個線程取出的字符串基本上是倒序的(因爲不同線程同時存元素,因此取的字符串打印到屏幕上往往不是倒序的了)
public class PriorityBlockingQueueExam { public static void main(String[] args) { //創建一個初始容量爲3,排序爲字符串排序相反的隊列 PriorityBlockingQueue<String> queue = new PriorityBlockingQueue<>(3, (o1, o2) -> { if (o1.compareTo(o2) < 0) { return 1; } else if (o1.compareTo(o2) > 0) { return -1; } else { return 0; } }); ExecutorService service = Executors.newCachedThreadPool(); for (int i = 0; i < 5; i++) { service.submit(new Producer("Producer" + i, queue)); } for (int i = 0; i < 5; i++) { service.submit(new Consumer("Consumer" + i, queue)); } service.shutdown(); } static class Producer implements Runnable { private final String name; private final PriorityBlockingQueue<String> queue; private static Random rand = new Random(System.currentTimeMillis()); Producer(String name, PriorityBlockingQueue<String> queue) { this.name = name; this.queue = queue; } @Override public void run() { for (int i = 0; i < 10; i++) { String str = "Product" + rand.nextInt(1000); queue.put(str); System.out.println("->" + name + " put " + str); try { TimeUnit.SECONDS.sleep(rand.nextInt(5)); } catch (InterruptedException e) { e.printStackTrace(); } } System.out.println(name + " is over"); } } static class Consumer implements Runnable { private final String name; private final PriorityBlockingQueue<String> queue; private static Random rand = new Random(System.currentTimeMillis()); Consumer(String name, PriorityBlockingQueue<String> queue) { this.name = name; this.queue = queue; } @Override public void run() { try { for (int i = 0; i < 10; i++) { String str = queue.take(); System.out.println("<-" + name + " take " + str); TimeUnit.SECONDS.sleep(rand.nextInt(5)); } System.out.println(name + " is over"); } catch (InterruptedException e) { e.printStackTrace(); } } } }
2.4 DelayQueue
隊列中只能存入Delayed接口實現的對象
DelayQueue
DelayQueue中存入的對象要同時實現getDelay和compareTo
-
getDelay方法是用來檢測隊列中的元素是否到期
-
compareTo方法是用來給隊列中的元素進行排序
DelayQueue持有一個PriorityBlockingQueue
每個Delayed對象實際上都放入了這個隊列,並按照compareTo方法進行排序
當隊列中對象的getDelay方法返回的值<=0(即對象已經超時)時,纔可以將對象從隊列中取出
若使用take方法,則方法會一直阻塞,直到隊列頭部的對象超時被取出
若使用poll方法,則當沒有超時對象時,直接返回null
總結來說,有如下幾個特點:
-
隊列中的對象都是Delayed對象,它實現了getDelay和compareTo
-
隊列中的對象按照優先級(按照compareTo)進行了排序,隊列頭部是最先超時的對象
-
take方法會在沒有超時對象時一直阻塞,直到有對象超時;poll方法會在沒有超時對象時返回null。
-
隊列中不允許存儲null,且iterator方法返回的值不能確保按順序排列
下面是一個列子,特別需要注意getDelay和compareTo方法的實現:
public class DelayQueueExam { public static void main(String[] args) throws InterruptedException { DelayQueue<DelayElement> queue = new DelayQueue<>(); for (int i = 0; i < 10; i++) { queue.put(new DelayElement(1000 * i, "DelayElement" + i)); } while (!queue.isEmpty()) { DelayElement delayElement = queue.take(); System.out.println(delayElement.getName()); } } static class DelayElement implements Delayed { private final long delay; private long expired; private final String name; DelayElement(int delay, String name) { this.delay = delay; this.name = name; expired = System.currentTimeMillis() + delay; } public String getName() { return name; } @Override public long getDelay(TimeUnit unit) { return unit.convert(expired - System.currentTimeMillis(), TimeUnit.MILLISECONDS); } @Override public int compareTo(Delayed o) { long d = (getDelay(TimeUnit.MILLISECONDS) - o.getDelay(TimeUnit.MILLISECONDS)); return (d == 0) ? 0 : ((d < 0) ? -1 : 1); } } }
DelayQueue通過PriorityQueue,使得超時的對象最先被處理,將take對象的操作阻塞住,避免了遍歷方式的輪詢,提高了性能。在很多需要回收超時對象的場景都能用上
BlockingDeque阻塞雙向隊列
BlockingDeque中各種特性上都非常類似於BlockingQueue,事實上它也繼承自BlockingQueue,它們的不同點主要在於BlockingDeque可以同時從隊列頭部和尾部增刪元素。
因此,總結一下BlockingDeque的四組增刪元素的方法:
第一組,拋異常的方法,包括addFirst(e),addLast(e),removeFirst(),removeLast(),getFirst()和getLast();
第二組,返回特殊值的方法,包括offerFirst(e) ,offerLast(e) ,pollFirst(),pollLast(),peekFirst()和peekLast();
第三組,阻塞的方法,包括putFirst(e),putLast(e),takeFirst()和takeLast();
第四組,超時的方法,包括offerFirst(e, time, unit),offerLast(e, time, unit),pollFirst(time, unit)和pollLast(time, unit)。
BlockingDeque目前只有一個實現類LinkedBlockingDeque,其用法與LinkedBlockingQueue非常類似,這裏就不給出實例了。
TransferQueue傳輸隊列
TransferQueue繼承自BlockingQueue,之所以將它獨立成章,是因爲它是一個非常重要的隊列,且提供了一些阻塞隊列所不具有的特性。
簡單來說,TransferQueue提供了一個場所,生產者線程使用transfer方法傳入一些對象並阻塞,直至這些對象被消費者線程全部取出。前面介紹的SynchronousQueue很像一個容量爲0的TransferQueue。
下面是一個例子,一個生產者使用transfer方法傳輸10個字符串,兩個消費者線程則各取出5個字符串,可以看到生產者在transfer時會一直阻塞直到所有字符串被取出:
public class TransferQueueExam { public static void main(String[] args) { TransferQueue<String> queue = new LinkedTransferQueue<>(); ExecutorService service = Executors.newCachedThreadPool(); service.submit(new Producer("Producer1", queue)); service.submit(new Consumer("Consumer1", queue)); service.submit(new Consumer("Consumer2", queue)); service.shutdown(); } static class Producer implements Runnable { private final String name; private final TransferQueue<String> queue; Producer(String name, TransferQueue<String> queue) { this.name = name; this.queue = queue; } @Override public void run() { System.out.println("begin transfer objects"); try { for (int i = 0; i < 10; i++) { queue.transfer("Product" + i); System.out.println(name + " transfer "+"Product"+i); } System.out.println("after transformation"); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(name + " is over"); } } static class Consumer implements Runnable { private final String name; private final TransferQueue<String> queue; private static Random rand = new Random(System.currentTimeMillis()); Consumer(String name, TransferQueue<String> queue) { this.name = name; this.queue = queue; } @Override public void run() { try { for (int i = 0; i < 5; i++) { String str = queue.take(); System.out.println(name + " take " + str); TimeUnit.SECONDS.sleep(rand.nextInt(5)); } System.out.println(name + " is over"); } catch (InterruptedException e) { e.printStackTrace(); } } } }
上面的代碼中只使用了transfer方法,TransferQueue共包括以下方法:
-
transfer(E e),若當前存在一個正在等待獲取的消費者線程,即立刻移交之;否則會將元素e插入到隊列尾部,並進入阻塞狀態,直到有消費者線程取走該元素。
-
tryTransfer(E e),若當前存在一個正在等待獲取的消費者線程(使用take()或者poll()函數),即立刻移交之; 否則返回false,並且不進入隊列,這是一個非阻塞的操作。
-
tryTransfer(E e, long timeout, TimeUnit unit) 若當前存在一個正在等待獲取的消費者線程,即立刻移交之;否則會將元素e插入到隊列尾部,並且等待被消費者線程獲取消費掉,若在指定的時間內元素e無法被消費者線程獲取,則返回false,同時該元素被移除。
-
hasWaitingConsumer() 判斷是否存在消費者線程。
-
getWaitingConsumerCount() 獲取所有等待獲取元素的消費線程數量。
再來看兩個生產者和兩個消費者的例子:
public class TransferQueueExam2 { public static void main(String[] args) { TransferQueue<String> queue = new LinkedTransferQueue<>(); ExecutorService service = Executors.newCachedThreadPool(); service.submit(new Producer("Producer1", queue)); service.submit(new Producer("Producer2", queue)); service.submit(new Consumer("Consumer1", queue)); service.submit(new Consumer("Consumer2", queue)); service.shutdown(); } static class Producer implements Runnable { private final String name; private final TransferQueue<String> queue; Producer(String name, TransferQueue<String> queue) { this.name = name; this.queue = queue; } @Override public void run() { System.out.println(name + " begin transfer objects"); try { for (int i = 0; i < 5; i++) { queue.transfer(name + "_Product" + i); System.out.println(name + " transfer " + name + "_Product" + i); } System.out.println(name + " after transformation"); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(name + " is over"); } } static class Consumer implements Runnable { private final String name; private final TransferQueue<String> queue; private static Random rand = new Random(System.currentTimeMillis()); Consumer(String name, TransferQueue<String> queue) { this.name = name; this.queue = queue; } @Override public void run() { try { for (int i = 0; i < 5; i++) { String str = queue.take(); System.out.println(name + " take " + str); TimeUnit.SECONDS.sleep(rand.nextInt(5)); } System.out.println(name + " is over"); } catch (InterruptedException e) { e.printStackTrace(); } } } }
它的作者Doug Lea 這樣評價它:TransferQueue是一個聰明的隊列,它是ConcurrentLinkedQueue, SynchronousQueue (在公平模式下), 無界的LinkedBlockingQueues等的超集。
所以,在合適的場景中,請儘量使用TransferQueue,目前它只有一個實現類LinkedTransferQueue。
ConcurrentLinkedQueue併發鏈接隊列
1 併發與並行
寫到此處時,應該認真梳理一下關於多線程編程中的一些名詞了,具體包括多線程(MultiThread)、併發(Concurrency)和並行(Parrellism)。多線程的概念應該是比較清晰的,是指計算機和編程語言都提供線程的概念,多個線程可以同時在一臺計算機中運行。
而併發和並行則是兩個非常容易混淆的概念,第一種區分方法是以程序在計算機中的執行方式來區分。我稱之爲“併發執行”和“並行執行”的區分:
併發執行是指多個線程(例如n個)在一臺計算機中宏觀上“同時”運行,它們有可能是一個CPU輪換的處理n個線程,也有可能是m個CPU以各種調度策略來輪換處理n個線程;
並行執行是指多個線程(n個)在一臺計算機的多個CPU(m個,m>=n)上微觀上同時運行,並行執行時操作系統不需要調度這n個線程,每個線程都獨享一個CPU持續運行直至結束。
第二種區分方法則是“併發編程”和“並行編程”的區別:
併發編程可以理解爲多線程編程,併發編程的代碼必定以“併發執行”的方式運行;
並行編程則是一種更加特殊的編程方法,它需要使用特殊的編程語言(例如Cilk語言),或者特殊的編程框架(例如Parallel Java 2 Library)。另外,我在本系列的第一篇中提到的Fork-Join框架也是一種並行編程框架。
2 併發的基礎
理解了併發的概念,我們再來看首次遇到的帶有併發字眼(Concurrent)的類ConcurrentLinkedQueue,併發鏈接隊列。
目前看來,可以這麼認爲,在java.util.concurrency包內,凡是帶有Concurrent字眼的類,都是以CAS爲基礎的非阻塞工具類。例如ConcurrentLinkedQueue、ConcurrentLinkedDeque、ConcurrentHashMap、ConcurrentSkipListMap、ConcurrentSkipListSet。
好了,那麼什麼是CAS呢?CAS即CompareAndSwap“比較並交換”,具體的細節將會在後續的關於原子變量(Atomic)章節中介紹。簡而言之,當代的很多CPU提供了一種CAS指令,由於指令運行不會被打斷,因此依賴這種指令就可以設計出一種不需要鎖的非阻塞併發算法,依賴這種算法,就可以設計出各種併發類。
3 各類隊列的例子
下面的例子中,我們使用參數控制,分別測試了四種隊列在多個線程同時存儲變量時的表現:
public class ConcurrentLinkedQueueExam { private static final int TEST_INT = 10000000; public static void main(String[] args) throws InterruptedException { long start = System.currentTimeMillis(); Queue<Integer> queue = null; if (args.length < 1) { System.out.println("Usage: input 1~4 "); System.exit(1); } int param = Integer.parseInt(args[0]); switch (param) { case 1: queue = new LinkedList<>(); break; case 2: queue = new LinkedBlockingQueue<>(); break; case 3: queue = new ArrayBlockingQueue<Integer>(TEST_INT * 5); break; case 4: queue = new ConcurrentLinkedQueue<>(); break; default: System.out.println("Usage: input 1~4 "); System.exit(2); } System.out.println("Using " + queue.getClass().getSimpleName()); ExecutorService service = Executors.newCachedThreadPool(); for (int i = 0; i < 5; i++) { service.submit(new Putter(queue, "Putter" + i)); } TimeUnit.SECONDS.sleep(2); for (int i = 0; i < 5; i++) { service.submit(new Getter(queue, "Getter" + i)); } service.shutdown(); service.awaitTermination(1, TimeUnit.DAYS); long end = System.currentTimeMillis(); System.out.println("Time span = " + (end - start)); System.out.println("queue size = " + queue.size()); } static class Putter implements Runnable { private final Queue<Integer> queue; private final String name; Putter(Queue<Integer> queue, String name) { this.queue = queue; this.name = name; } @Override public void run() { for (int i = 0; i < TEST_INT; i++) { queue.offer(1); } System.out.println(name + " is over"); } } static class Getter implements Runnable { private final Queue<Integer> queue; private final String name; Getter(Queue<Integer> queue, String name) { this.queue = queue; this.name = name; } @Override public void run() { int i = 0; while (i < TEST_INT) { synchronized (Getter.class) { if (!queue.isEmpty()) { queue.poll(); i++; } } } System.out.println(name + " is over"); } } }
輸入1,結果如下: Using LinkedList … Time span = 16613queue size = 10296577輸入2,結果如下: Using LinkedBlockingQueue … Time span = 16847queue size = 0輸入3,結果如下: Using ArrayBlockingQueue … Time span = 6815queue size = 0輸入4,結果如下: Using ConcurrentLinkedQueue … Time span = 22802queue size = 0
分析運行的結果,有如下結論:
第一,非併發類例如LinkedList在多線程環境下運行是會出錯的,結果的最後一行輸出了隊列的size值,只有它的size值不等於0,這說明在多線程運行時許多poll操作並沒有彈出元素,甚至很多offer操作也沒有能夠正確插入元素。其他三種併發類都能夠在多線程環境下正確運行;
第二,併發類也不是完全不需要注意加鎖,例如這一段代碼:
while (i < TEST_INT) { synchronized (Getter.class) { if (!queue.isEmpty()) { queue.poll(); i++; } } }
如果不加鎖,那麼isEmpty和poll之間有可能被其他線程打斷,造成結果的不確定性。
第三,本例中LinkedBlockingQueue和ArrayBlockingQueue並沒有因爲生產-消費關係阻塞,因爲容量設置得足夠大。它們的元素插入和彈出操作是加鎖的,而ConcurrentLinkedQueue的元素插入和彈出操作是不加鎖的,而觀察性能其實並沒有數量級上的差異(有待進一步測試)。
第四,ArrayBlockingQueue性能明顯好於LinkedBlockingQueue,甚至也好於ConcurrentLinkedQueue,這是因爲它的內部存儲結構是原生數組,而其他兩個是鏈表,需要new一個Node。同時,鏈表也會造成更多的GC。
ConcurrentLinkedDeque併發鏈接雙向隊列
ConcurrentLinkedDeque與ConcurrentLinkedQueue非常類似,不同之處僅在於它是一個雙向隊列。
3 阻塞隊列的實現原理
Java的併發隊列,具體包括BlockingQueue阻塞隊列、BlockingDeque阻塞雙向隊列、TransferQueue傳輸隊列、ConcurrentLinkedQueue併發鏈接隊列和ConcurrentLinkedDeque併發鏈接雙向隊列。BlockingQueue和BlockingDeque的內部使用鎖來保護元素的插入彈出操作,同時它們還提供了生產者-消費者場景的阻塞方法;TransferQueue被用來在多個線程之間優雅的傳遞對象;ConcurrentLinkedQueue和ConcurrentLinkedDeque依靠CAS指令,來實現非阻塞的併發算法。
若隊列爲空,消費者會一直等待,當生產者添加元素時,消費者是如何知道當前隊列有元素的呢?讓我們看看JDK是如何實現的。
使用通知模式實現。所謂通知模式,就是當生產者往滿的隊列裏添加元素時會阻塞住生產者,當消費者消費了一個隊列中的元素後,會通知生產者當前隊列可用。通過查看源碼發現ArrayBlockingQueue使用了Condition來實現,代碼如下。
private final Condition notFull; private final Condition notEmpty; public ArrayBlockingQueue(int capacity, boolean fair) { // 省略其他代碼 notEmpty = lock.newCondition(); notFull = lock.newCondition(); public void put(E e) throws InterruptedException { checkNotNull(e); final ReentrantLock lock = this.lock; lock.lockInterruptibly(); try { while (count == items.length) notFull.await(); enqueue(e); } finally { lock.unlock(); } } public E take() throws InterruptedException { final ReentrantLock lock = this.lock; lock.lockInterruptibly(); try { while (count == 0) notEmpty.await(); return dequeue(); } finally { lock.unlock(); } } private void enqueue(E x) { // assert lock.getHoldCount() == 1; // assert items[putIndex] == null; final Object[] items = this.items; items[putIndex] = x; if (++putIndex == items.length) putIndex = 0; count++; notEmpty.signal(); }
當往隊列裏插入一個元素時,如果隊列不可用,那麼阻塞生產者主要通過
LockSupport.park(this)來實現。
public final void await() throws InterruptedException { if (Thread.interrupted()) throw new InterruptedException(); Node node = addConditionWaiter(); int savedState = fullyRelease(node); int interruptMode = 0; while (!isOnSyncQueue(node)) { LockSupport.park(this); if ((interruptMode = checkInterruptWhileWaiting(node)) != 0) break; } if (acquireQueued(node, savedState) && interruptMode != THROW_IE) interruptMode = REINTERRUPT; if (node.nextWaiter != null) // clean up if cancelled unlinkCancelledWaiters(); if (interruptMode != 0) reportInterruptAfterWait(interruptMode); }
繼續進入源碼,發現調用setBlocker先保存一下將要阻塞的線程,然後調用unsafe.park阻塞當前線程。
public static void park(Object blocker) { Thread t = Thread.currentThread(); setBlocker(t, blocker); unsafe.park(false, 0L); setBlocker(t, null); }
unsafe.park是個native方法,代碼如下。
public native void park(boolean isAbsolute, long time);
park這個方法會阻塞當前線程,只有以下4種情況中的一種發生時,該方法纔會返回。
-
與park對應的unpark執行或已經執行時。“已經執行”是指unpark先執行,然後再執行park的情況。
-
線程被中斷時。
-
等待完time參數指定的毫秒數時。
-
異常現象發生時,這個異常現象沒有任何原因。