一、前言
分析完了ArrayBlockingQueue後,接著分析LinkedBlockingQueue,與ArrayBlockingQueue不相同,LinkedBlockingQueue底層採用的是連結串列結構,其原始碼也相對比較簡單,下面進行正式的分析。
二、LinkedBlockingQueue資料結構
從LinkedBlockingQueue的命名就大致知道其資料結構採用的是連結串列結構,通過原始碼也可以驗證我們的猜測,其資料結構如下。
說明:可以看到LinkedBlockingQueue採用的是單連結串列結構,包含了頭結點和尾節點。
三、LinkedBlockingQueue原始碼分析
3.1 類的繼承關係
public class LinkedBlockingQueue<E> extends AbstractQueue<E> implements BlockingQueue<E>, java.io.Serializable {}
說明:LinkedBlockingQueue繼承了AbstractQueue抽象類,AbstractQueue定義了對佇列的基本操作;同時實現了BlockingQueue介面,BlockingQueue表示阻塞型的佇列,其對佇列的操作可能會丟擲異常;同時也實現了Searializable介面,表示可以被序列化。
3.2 類的內部類
LinkedBlockingQueue內部有一個Node類,表示結點,用於存放元素,其原始碼如下。
static class Node<E> { // 元素 E item; // next域 Node<E> next; // 建構函式 Node(E x) { item = x; } }
說明:Node類非常簡單,包含了兩個域,分別用於存放元素和指示下一個結點。
3.3 類的屬性
![](https://i.iter01.com/images/86153736c3d440f2e7d2df2432d97112d2ef05f0cb7522cf2307c3de9447249a.gif)
public class LinkedBlockingQueue<E> extends AbstractQueue<E> implements BlockingQueue<E>, java.io.Serializable { // 版本序列號 private static final long serialVersionUID = -6903933977591709194L; // 容量 private final int capacity; // 元素的個數 private final AtomicInteger count = new AtomicInteger(); // 頭結點 transient Node<E> head; // 尾結點 private transient Node<E> last; // 取元素鎖 private final ReentrantLock takeLock = new ReentrantLock(); // 非空條件 private final Condition notEmpty = takeLock.newCondition(); // 存元素鎖 private final ReentrantLock putLock = new ReentrantLock(); // 非滿條件 private final Condition notFull = putLock.newCondition(); }
說明:可以看到LinkedBlockingQueue包含了讀、寫重入鎖(與ArrayBlockingQueue不同,ArrayBlockingQueue只包含了一把重入鎖),讀寫操作進行了分離,並且不同的鎖有不同的Condition條件(與ArrayBlockingQueue不同,ArrayBlockingQueue是一把重入鎖的兩個條件)。
3.4 類的建構函式
1. LinkedBlockingQueue()型建構函式
![](https://i.iter01.com/images/86153736c3d440f2e7d2df2432d97112d2ef05f0cb7522cf2307c3de9447249a.gif)
public LinkedBlockingQueue() { this(Integer.MAX_VALUE); }
說明:該建構函式用於建立一個容量為 Integer.MAX_VALUE 的 LinkedBlockingQueue。
2. LinkedBlockingQueue(int)型建構函式
![](https://i.iter01.com/images/86153736c3d440f2e7d2df2432d97112d2ef05f0cb7522cf2307c3de9447249a.gif)
public LinkedBlockingQueue(int capacity) { // 初始化容量必須大於0 if (capacity <= 0) throw new IllegalArgumentException(); // 初始化容量 this.capacity = capacity; // 初始化頭結點和尾結點 last = head = new Node<E>(null); }
說明:該建構函式用於建立一個具有給定(固定)容量的 LinkedBlockingQueue。
3. LinkedBlockingQueue(Collection<? extends E>)型建構函式
![](https://i.iter01.com/images/86153736c3d440f2e7d2df2432d97112d2ef05f0cb7522cf2307c3de9447249a.gif)
public LinkedBlockingQueue(Collection<? extends E> c) { // 呼叫過載建構函式 this(Integer.MAX_VALUE); // 存鎖 final ReentrantLock putLock = this.putLock; // 獲取鎖 putLock.lock(); // Never contended, but necessary for visibility try { int n = 0; for (E e : c) { // 遍歷c集合 if (e == null) // 元素為null,丟擲異常 throw new NullPointerException(); if (n == capacity) // throw new IllegalStateException("Queue full"); enqueue(new Node<E>(e)); ++n; } count.set(n); } finally { putLock.unlock(); } }
說明:該建構函式用於建立一個容量是 Integer.MAX_VALUE 的 LinkedBlockingQueue,最初包含給定 collection 的元素,元素按該 collection 迭代器的遍歷順序新增。
3.5 核心函式分析
1. put函式
![](https://i.iter01.com/images/86153736c3d440f2e7d2df2432d97112d2ef05f0cb7522cf2307c3de9447249a.gif)
public void put(E e) throws InterruptedException { // 值不為空 if (e == null) throw new NullPointerException(); // Note: convention in all put/take/etc is to preset local var // holding count negative to indicate failure unless set. // int c = -1; // 新生結點 Node<E> node = new Node<E>(e); // 存元素鎖 final ReentrantLock putLock = this.putLock; // 元素個數 final AtomicInteger count = this.count; // 如果當前執行緒未被中斷,則獲取鎖 putLock.lockInterruptibly(); try { /* * Note that count is used in wait guard even though it is * not protected by lock. This works because count can * only decrease at this point (all other puts are shut * out by lock), and we (or some other waiting put) are * signalled if it ever changes from capacity. Similarly * for all other uses of count in other wait guards. */ while (count.get() == capacity) { // 元素個數到達指定容量 // 在notFull條件上進行等待 notFull.await(); } // 入佇列 enqueue(node); // 更新元素個數,返回的是以前的元素個數 c = count.getAndIncrement(); if (c + 1 < capacity) // 元素個數是否小於容量 // 喚醒在notFull條件上等待的某個執行緒 notFull.signal(); } finally { // 釋放鎖 putLock.unlock(); } if (c == 0) // 元素個數為0,表示已有take執行緒在notEmpty條件上進入了等待,則需要喚醒在notEmpty條件上等待的執行緒 signalNotEmpty(); }
說明:put函式用於存放元素,其流程如下。
① 判斷元素是否為null,若是,則丟擲異常,否則,進入步驟②
② 獲取存元素鎖,並上鎖,如果當前執行緒被中斷,則丟擲異常,否則,進入步驟③
③ 判斷當前佇列中的元素個數是否已經達到指定容量,若是,則在notFull條件上進行等待,否則,進入步驟④
④ 將新生結點入佇列,更新佇列元素個數,若元素個數小於指定容量,則喚醒在notFull條件上等待的執行緒,表示可以繼續存放元素。進入步驟⑤
⑤ 釋放鎖,判斷結點入佇列之前的元素個數是否為0,若是,則喚醒在notEmpty條件上等待的執行緒(表示佇列中沒有元素,取元素執行緒被阻塞了)。
put函式中會呼叫到enqueue函式和signalNotEmpty函式,enqueue函式原始碼如下
![](https://i.iter01.com/images/86153736c3d440f2e7d2df2432d97112d2ef05f0cb7522cf2307c3de9447249a.gif)
private void enqueue(Node<E> node) { // assert putLock.isHeldByCurrentThread(); // assert last.next == null; // 更新尾結點域 last = last.next = node; }
說明:可以看到,enqueue函式只是更新了尾節點。signalNotEmpty函式原始碼如下
![](https://i.iter01.com/images/86153736c3d440f2e7d2df2432d97112d2ef05f0cb7522cf2307c3de9447249a.gif)
private void signalNotEmpty() { // 取元素鎖 final ReentrantLock takeLock = this.takeLock; // 獲取鎖 takeLock.lock(); try { // 喚醒在notEmpty條件上等待的某個執行緒 notEmpty.signal(); } finally { // 釋放鎖 takeLock.unlock(); } }
說明:signalNotEmpty函式用於喚醒在notEmpty條件上等待的執行緒,其首先獲取取元素鎖,然後上鎖,然後喚醒在notEmpty條件上等待的執行緒,最後釋放取元素鎖。
2. offer函式
![](https://i.iter01.com/images/86153736c3d440f2e7d2df2432d97112d2ef05f0cb7522cf2307c3de9447249a.gif)
public boolean offer(E e) { // 確保元素不為null if (e == null) throw new NullPointerException(); // 獲取計數器 final AtomicInteger count = this.count; if (count.get() == capacity) // 元素個數到達指定容量 // 返回 return false; // int c = -1; // 新生結點 Node<E> node = new Node<E>(e); // 存元素鎖 final ReentrantLock putLock = this.putLock; // 獲取鎖 putLock.lock(); try { if (count.get() < capacity) { // 元素個數小於指定容量 // 入佇列 enqueue(node); // 更新元素個數,返回的是以前的元素個數 c = count.getAndIncrement(); if (c + 1 < capacity) // 元素個數是否小於容量 // 喚醒在notFull條件上等待的某個執行緒 notFull.signal(); } } finally { // 釋放鎖 putLock.unlock(); } if (c == 0) // 元素個數為0,則喚醒在notEmpty條件上等待的某個執行緒 signalNotEmpty(); return c >= 0; }
說明:offer函式也用於存放元素,offer函式新增元素不會丟擲異常(其他的域put函式類似)。
3. take函式
![](https://i.iter01.com/images/86153736c3d440f2e7d2df2432d97112d2ef05f0cb7522cf2307c3de9447249a.gif)
public E take() throws InterruptedException { E x; int c = -1; // 獲取計數器 final AtomicInteger count = this.count; // 獲取取元素鎖 final ReentrantLock takeLock = this.takeLock; // 如果當前執行緒未被中斷,則獲取鎖 takeLock.lockInterruptibly(); try { while (count.get() == 0) { // 元素個數為0 // 在notEmpty條件上等待 notEmpty.await(); } // 出佇列 x = dequeue(); // 更新元素個數,返回的是以前的元素個數 c = count.getAndDecrement(); if (c > 1) // 元素個數大於1,則喚醒在notEmpty上等待的某個執行緒 notEmpty.signal(); } finally { // 釋放鎖 takeLock.unlock(); } if (c == capacity) // 元素個數到達指定容量 // 喚醒在notFull條件上等待的某個執行緒 signalNotFull(); // 返回 return x; }
說明:take函式用於獲取一個元素,其與put函式相對應,其流程如下。
① 獲取取元素鎖,並上鎖,如果當前執行緒被中斷,則丟擲異常,否則,進入步驟②
② 判斷當前佇列中的元素個數是否為0,若是,則在notEmpty條件上進行等待,否則,進入步驟③
③ 出佇列,更新佇列元素個數,若元素個數大於1,則喚醒在notEmpty條件上等待的執行緒,表示可以繼續取元素。進入步驟④
④ 釋放鎖,判斷結點出佇列之前的元素個數是否為指定容量,若是,則喚醒在notFull條件上等待的執行緒(表示佇列已滿,存元素執行緒被阻塞了)。
take函式呼叫到了dequeue函式和signalNotFull函式,dequeue函式原始碼如下
![](https://i.iter01.com/images/86153736c3d440f2e7d2df2432d97112d2ef05f0cb7522cf2307c3de9447249a.gif)
private E dequeue() { // assert takeLock.isHeldByCurrentThread(); // assert head.item == null; // 頭結點 Node<E> h = head; // 第一個結點 Node<E> first = h.next; // 頭結點的next域為自身 h.next = h; // help GC // 更新頭結點 head = first; // 返回頭結點的元素 E x = first.item; // 頭結點的item域賦值為null first.item = null; // 返回結點元素 return x; }
說明:dequeue函式的作用是將頭結點更新為之前頭結點的下一個結點,並且將更新後的頭結點的item域設定為null。signalNotFull函式的原始碼如下
![](https://i.iter01.com/images/86153736c3d440f2e7d2df2432d97112d2ef05f0cb7522cf2307c3de9447249a.gif)
private void signalNotFull() { // 存元素鎖 final ReentrantLock putLock = this.putLock; // 獲取鎖 putLock.lock(); try { // 喚醒在notFull條件上等待的某個執行緒 notFull.signal(); } finally { // 釋放鎖 putLock.unlock(); } }
說明:signalNotFull函式用於喚醒在notFull條件上等待的某個執行緒,其首先獲取存元素鎖,然後上鎖,然後喚醒在notFull條件上等待的執行緒,最後釋放存元素鎖。
4. poll函式
![](https://i.iter01.com/images/86153736c3d440f2e7d2df2432d97112d2ef05f0cb7522cf2307c3de9447249a.gif)
public E poll() { // 獲取計數器 final AtomicInteger count = this.count; if (count.get() == 0) // 元素個數為0 return null; // E x = null; int c = -1; // 取元素鎖 final ReentrantLock takeLock = this.takeLock; // 獲取鎖 takeLock.lock(); try { if (count.get() > 0) { // 元素個數大於0 // 出佇列 x = dequeue(); // 更新元素個數,返回的是以前的元素個數 c = count.getAndDecrement(); if (c > 1) // 元素個數大於1 // 喚醒在notEmpty條件上等待的某個執行緒 notEmpty.signal(); } } finally { // 釋放鎖 takeLock.unlock(); } if (c == capacity) // 元素大小達到指定容量 // 喚醒在notFull條件上等待的某個執行緒 signalNotFull(); // 返回元素 return x; }
說明:poll函式也用於存放元素,poll函式新增元素不會丟擲異常(其他的與take函式類似)。
5. remove函式
![](https://i.iter01.com/images/86153736c3d440f2e7d2df2432d97112d2ef05f0cb7522cf2307c3de9447249a.gif)
public boolean remove(Object o) { // 元素為null,返回false if (o == null) return false; // 獲取存元素鎖和取元素鎖(不允許存或取元素) fullyLock(); try { for (Node<E> trail = head, p = trail.next; p != null; trail = p, p = p.next) { // 遍歷整個連結串列 if (o.equals(p.item)) { // 結點的值與指定值相等 // 斷開結點 unlink(p, trail); return true; } } return false; } finally { fullyUnlock(); } }
說明:remove函式的流程如下
① 獲取讀、寫鎖(防止此時繼續出、入佇列)。進入步驟②
② 遍歷連結串列,尋找指定元素,若找到,則將該結點從連結串列中斷開,有利於被GC,進入步驟③
③ 釋放讀、寫鎖(可以繼續出、入佇列)。步驟②中找到指定元素則返回true,否則,返回false。
其中,remove函式會呼叫unlink函式,其原始碼如下
![](https://i.iter01.com/images/86153736c3d440f2e7d2df2432d97112d2ef05f0cb7522cf2307c3de9447249a.gif)
void unlink(Node<E> p, Node<E> trail) { // assert isFullyLocked(); // p.next is not changed, to allow iterators that are // traversing p to maintain their weak-consistency guarantee. // 結點的item域賦值為null p.item = null; // 斷開p結點 trail.next = p.next; if (last == p) // 尾節點為p結點 // 重新賦值尾節點 last = trail; if (count.getAndDecrement() == capacity) // 更新元素個數,返回的是以前的元素個數,若結點個數到達指定容量 // 喚醒在notFull條件上等待的某個執行緒 notFull.signal(); }
說明:unlink函式用於將指定結點從連結串列中斷開,並且更新佇列元素個數,並且判斷若之前佇列元素的個數達到了指定容量,則會喚醒在notFull條件上等待的某個執行緒。
四、示例
下面通過一個示例來了解LinkedBlockingQueue的使用。
![](https://i.iter01.com/images/86153736c3d440f2e7d2df2432d97112d2ef05f0cb7522cf2307c3de9447249a.gif)
package com.hust.grid.leesf.collections; import java.util.concurrent.LinkedBlockingQueue; class PutThread extends Thread { private LinkedBlockingQueue<Integer> lbq; public PutThread(LinkedBlockingQueue<Integer> lbq) { this.lbq = lbq; } public void run() { for (int i = 0; i < 10; i++) { try { System.out.println("put " + i); lbq.put(i); Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } } } } class GetThread extends Thread { private LinkedBlockingQueue<Integer> lbq; public GetThread(LinkedBlockingQueue<Integer> lbq) { this.lbq = lbq; } public void run() { for (int i = 0; i < 10; i++) { try { System.out.println("take " + lbq.take()); Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } } } } public class LinkedBlockingQueueDemo { public static void main(String[] args) { LinkedBlockingQueue<Integer> lbq = new LinkedBlockingQueue<Integer>(); PutThread p1 = new PutThread(lbq); GetThread g1 = new GetThread(lbq); p1.start(); g1.start(); } }
執行結果:
![](https://i.iter01.com/images/86153736c3d440f2e7d2df2432d97112d2ef05f0cb7522cf2307c3de9447249a.gif)
put 0 take 0 put 1 take 1 put 2 take 2 put 3 take 3 put 4 take 4 put 5 take 5 put 6 take 6 put 7 take 7 put 8 take 8 put 9 take 9
說明:示例中使用了兩個執行緒,一個用於存元素,一個用於讀元素,存和讀各10次,每個執行緒存一個元素或者讀一個元素後都會休眠100ms,可以看到結果是交替打 印,並且首先列印的肯定是put執行緒語句(因為若取執行緒先取元素,此時佇列並沒有元素,其會阻塞,等待存執行緒存入元素),並且最終程式可以正常結束。
① 若修改取元素執行緒,將存的元素的次數修改為15次(for迴圈的結束條件改為15即可),執行結果如下:
![](https://i.iter01.com/images/86153736c3d440f2e7d2df2432d97112d2ef05f0cb7522cf2307c3de9447249a.gif)
put 0 take 0 put 1 take 1 put 2 take 2 put 3 take 3 put 4 take 4 put 5 take 5 put 6 take 6 put 7 take 7 put 8 take 8 put 9 take 9
說明:執行結果與上面的執行結果相同,但是,此時程式無法正常結束,因為take方法被阻塞了,等待被喚醒。
五、總結
LinkedBlockingQueue的原始碼相對比較簡單,其也是通過ReentrantLock和Condition條件來保證多執行緒的正確訪問的,並且取元素(出佇列)和存元素(入佇列)是採用不同的鎖,進行了讀寫分離,有利於提高併發度。LinkedBockingQueue的分析就到這裡,歡迎交流,謝謝各位園友的觀看~