LinkedBlockingQueue 和 ConcurrentLinkedQueue的區別
- 2020 年 8 月 19 日
- 筆記
- JAVA, microservice, spring, Spring Boot, SpringMVC, 微服務
1. 簡單的開篇
LinkedBlockingQueue 和 ConcurrentLinkedQueue 是 Java 高並發場景中最常使用的隊列。儘管這兩個隊列經常被用作並發場景的數據結構,但它們之間仍有細微的特徵和行為差異。
在這篇文章中,我將和大家一起探討這兩者之間的異同點。歡迎大家在留言討論~
2. LinkedBlockingQueue
首先 LinkedBlockingQueue 是一個 「可選且有界」 的阻塞隊列實現,你可以根據需要指定隊列的大小。
接下來,我將創建一個LinkedBlockingQueue,它最多可以包含100個元素:
BlockingQueue<Integer> boundedQueue = new LinkedBlockingQueue<>(100);
當然,我們也可以通過不指定大小,來創建一個無界的 LinkedBlockingQueue:
BlockingQueue<Integer> unboundedQueue = new LinkedBlockingQueue<>();
無界隊列表示在創建時未指定隊列的大小。因此,隊列可以隨著元素的添加而動態增長。但是,如果沒有剩餘記憶體,則隊列將拋出 java.lang.OutOfMemory 錯誤。
這裡留下一個問題給大家思考: 創建無界隊列是好還是壞呢?
我們還可以從現有的集合來創建 LinkedBlockingQueue:
Collection<Integer> listOfNumbers = Arrays.asList(1,2,3,4,5);
BlockingQueue<Integer> queue = new LinkedBlockingQueue<>(listOfNumbers);
LinkedBlockingQueue 實現了BlockingQueue介面,該介面為它提供了阻塞性質。
阻塞隊列表示如果訪問執行緒已滿(當隊列有界時)或變為空,則隊列將阻塞該執行緒。如果隊列已滿,則添加新元素將阻塞訪問執行緒,除非新元素有可用空間。類似地,如果隊列為空,則訪問元素會阻塞調用執行緒:
ExecutorService executorService = Executors.newFixedThreadPool(1);
LinkedBlockingQueue<Integer> queue = new LinkedBlockingQueue<>();
executorService.submit(() -> {
try {
queue.take();
}
catch (InterruptedException e) {
// exception handling
}
});
在上面的程式碼片段中,我們正在訪問一個空隊列。因此,take() 方法阻塞調用執行緒。
LinkedBlockingQueue 的阻塞特性與一些開銷相關。這個代價是因為每個put或take操作在生產者執行緒或使用者執行緒之間都是鎖爭用的。因此,在許多生產者和消費者的情況下,put和take 動作可能會慢一些。
3. ConcurrentLinkedQueue
首先聲明,ConcurrentLinkedQueue 是一個無邊界、執行緒安全且無阻塞的隊列
創建一個空的 ConcurrentLinkedQueue:
ConcurrentLinkedQueue queue = new ConcurrentLinkedQueue();
同上面一樣,我們也可以從現有集合創建 ConcurrentLinkedQueue:
Collection<Integer> listOfNumbers = Arrays.asList(1,2,3,4,5);
ConcurrentLinkedQueue<Integer> queue = new ConcurrentLinkedQueue<>(listOfNumbers);
不同於 LinkedBlockingQueue, ConcurrentLinkedQueue是非阻塞的隊列。因此,即使隊列為空(empty),它也不會阻塞執行緒。相反,它會返回 空(null) 。雖然它是無界的,但如果沒有額外的記憶體來添加新元素,它依舊會拋出 java.lang.OutOfMemory 錯誤。
除了非阻塞之外,ConcurrentLinkedQueue還有其他特性。
在任何生產者-消費者場景中,消費者都不會滿足於生產者;但是,多個生產者將相互競爭:
int element = 1;
ExecutorService executorService = Executors.newFixedThreadPool(2);
ConcurrentLinkedQueue<Integer> queue = new ConcurrentLinkedQueue<>();
Runnable offerTask = () -> queue.offer(element);
Callable<Integer> pollTask = () -> {
while (queue.peek() != null) {
return queue.poll().intValue();
}
return null;
};
executorService.submit(offerTask);
Future<Integer> returnedElement = executorService.submit(pollTask);
assertThat(returnedElement.get().intValue(), is(equalTo(element)));
第一個任務 offerTask 向隊列中添加元素,第二個任務 pollTask 從隊列中檢索元素。pollTask 首先檢查隊列中的元素,因為ConcurrentLinkedQueue是非阻塞的,並且可以返回null值。
4. 求同
LinkedBlockingQueue 和 ConcurrentLinkedQueue 都是隊列實現,並具有一些共同特徵。讓我們討論一下這兩個隊列的相似之處:
- 都 實現 Queue 介面
- 它們都使用 linked nodes 存儲節點
- 都適用於並發訪問場景
5. 存異
儘管這兩種隊列都有某些相似之處,但也有一些實質性的特徵差異:
特性 | LinkedBlockingQueue | ConcurrentLinkedQueue |
---|---|---|
阻塞性 | 阻塞隊列,並實現blocking queue介面 | 非阻塞隊列,不實現blocking queue介面 |
隊列大小 | 可選的有界隊列,這意味著可以在創建期間定義隊列大小 | 無邊界隊列,並且沒有在創建期間指定隊列大小的規定 |
鎖特性 | 基於鎖的隊列 | 無鎖隊列 |
演算法 | 鎖的實現基於 「雙鎖隊列(two lock queue)」 演算法 | 依賴於Michael&Scott演算法來實現無阻塞、無鎖隊列 |
實現 | 在 雙鎖隊列 演算法機制中,LinkedBlockingQueue使用兩種不同的鎖,putLock和takeLock。put/take操作使用第一個鎖類型,take/poll操作使用另一個鎖類型 | 使用CAS(Compare And Swap)進行操作 |
阻塞行為 | 當隊列為空時,它會阻塞訪問執行緒 | 當隊列為空時返回 null,它不會阻塞訪問執行緒 |
6. 總結才能進步
首先,我們分別討論了這兩種隊列實現及其一些特性、相似性、以及它們之間的差異。這樣的比較,是否讓你對這兩種隊列有了更深刻的印象?
公眾號: 鍋外的大佬 ,歡迎加群~
部落格地址: //www.developlee.top