LinkedBlockingQueue 和 ConcurrentLinkedQueue的區別

1. 簡單的開篇

LinkedBlockingQueueConcurrentLinkedQueue 是 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 的阻塞特性與一些開銷相關。這個代價是因為每個puttake操作在生產者線程或使用者線程之間都是鎖爭用的。因此,在許多生產者和消費者的情況下,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);

不同於 LinkedBlockingQueueConcurrentLinkedQueue是非阻塞的隊列。因此,即使隊列為空(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. 求同

LinkedBlockingQueueConcurrentLinkedQueue 都是隊列實現,並具有一些共同特徵。讓我們討論一下這兩個隊列的相似之處:

  1. 實現 Queue 接口
  2. 它們都使用 linked nodes 存儲節點
  3. 都適用於並發訪問場景

5. 存異

儘管這兩種隊列都有某些相似之處,但也有一些實質性的特徵差異:

特性 LinkedBlockingQueue ConcurrentLinkedQueue
阻塞性 阻塞隊列,並實現blocking queue接口 非阻塞隊列,不實現blocking queue接口
隊列大小 可選的有界隊列,這意味着可以在創建期間定義隊列大小 無邊界隊列,並且沒有在創建期間指定隊列大小的規定
鎖特性 基於鎖的隊列 無鎖隊列
算法 鎖的實現基於 「雙鎖隊列(two lock queue)」 算法 依賴於Michael&Scott算法來實現無阻塞、無鎖隊列
實現 雙鎖隊列 算法機制中,LinkedBlockingQueue使用兩種不同的鎖,putLocktakeLockput/take操作使用第一個鎖類型,take/poll操作使用另一個鎖類型 使用CAS(Compare And Swap)進行操作
阻塞行為 當隊列為空時,它會阻塞訪問線程 當隊列為空時返回 null,它不會阻塞訪問線程

6. 總結才能進步

首先,我們分別討論了這兩種隊列實現及其一些特性、相似性、以及它們之間的差異。這樣的比較,是否讓你對這兩種隊列有了更深刻的印象?

公眾號: 鍋外的大佬 ,歡迎加群~
博客地址: //www.developlee.top