JUC原始碼分析-集合篇(九):LinkedBlockingQueue
LinkedBlockingQueue 是單向連結串列結構的自定義容量的阻塞佇列,元素操作按照** FIFO **(first-in-first-out 先入先出) 的順序,使用顯式鎖 ReentrantLock 和 Condition 來保證執行緒安全。連結串列結構的佇列通常比基於陣列的佇列(ArrayBlockingQueue)有更高的吞吐量,但是在併發環境下效能卻不如陣列佇列。因為比較簡單,本章本來是不在筆者的寫作範圍內的,但是在後面的執行緒池原始碼中用到了LinkedBlockingQueue,我們我們就來簡單看一下,加深一下印象。
本章應該是佇列篇的終章了,還有LinkedBlockingDeque、ArrayBlockingQueue這些比較簡單的佇列就不再講解了,後面我們會開始執行緒池相關原始碼分析。
概述
LinkedBlockingQueue(後稱LBQ)佇列容量可通過引數來自定義,並且內部是不會自動擴容的。如果未指定容量,將取最大容量
Integer.MAX_VALUE
。 如果你理解了前幾篇我們所講的佇列,那麼你會發現 LBQ 非常容易理解,內部沒有太多複雜的演算法,資料結構也是使用了簡單的連結串列結構。
資料結構
LinkedBlockingQueue 繼承關係
標準的佇列繼承關係,不多贅述。
重要屬性
//容量 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();
LBQ 在實現多執行緒對競爭資源的互斥訪問時,對於入列和出列操作分別使用了不同的鎖。對於入列操作,通過putLock
進行同步;對於出列操作,通過takeLock
進行同步。
此外,插入鎖putLock
和出列條件notFull
相關聯,出列鎖takeLock
和出列條件notEmpty
相關聯。通過notFull
和notEmpty
更細膩的控制鎖。
- 若某執行緒(執行緒A)要取出資料時,佇列正好為空,則該執行緒會執行
notEmpty.await()
進行等待;當其它某個執行緒(執行緒B)向佇列中插入了資料之後,會呼叫notEmpty.signal()
喚醒notEmpty
上的等待執行緒。此時,執行緒A會被喚醒從而得以繼續執行。 此外,執行緒A在執行取操作前,會獲取takeLock
takeLock
。 - 若某執行緒(執行緒H)要插入資料時,佇列已滿,則該執行緒會它執行
notFull.await()
進行等待;當其它某個執行緒(執行緒I)取出資料之後,會呼叫notFull.signal()
喚醒notFull
上的等待執行緒。此時,執行緒H就會被喚醒從而得以繼續執行。 此外,執行緒H在執行插入操作前,會獲取putLock
,在插入操作執行完畢才釋放putLock
。
原始碼解析
put(E e)
LBQ 的新增元素的方法有offer()、put()
,put
是在佇列已滿的情況下等待,而offer
則直接返回結果,它們內部操作都一致。所這裡我們只對put
進行解析
//尾部插入節點,佇列滿時會一直等待可用,響應中斷
public void put(E e) throws InterruptedException {
if (e == null) throw new NullPointerException();
int c = -1;
Node<E> node = new Node<E>(e);
final ReentrantLock putLock = this.putLock;//獲取入列鎖
final AtomicInteger count = this.count;//獲取元素數
putLock.lockInterruptibly();//響應中斷式加鎖
try {
while (count.get() == capacity) {
notFull.await();//佇列已滿,等待
}
enqueue(node);//節點新增到佇列尾
c = count.getAndIncrement();
if (c + 1 < capacity)
notFull.signal();
} finally {
putLock.unlock();
}
if (c == 0)
signalNotEmpty();
}
說明:看原始碼吧。
poll()
LBQ 的獲取元素的方法有poll()、take()、peek()
,take
在佇列為空的情況下會一直等待,poll
不等待直接返回結果,peek
是獲取但不移除頭結點元素,內部操作都差不多。這裡我們只對take
進行解析:
/**獲取並消除頭節點,會一直等待佇列可用,響應中斷*/
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) {
notEmpty.await();//佇列為空,等待
}
x = dequeue();//首節點出列
c = count.getAndDecrement();
if (c > 1)
notEmpty.signal();
} finally {
takeLock.unlock();
}
if (c == capacity)
signalNotFull();
return x;
}
說明:略。。。
小結
本章只是為了加深同學們的印象,為之後執行緒池原始碼解析做準備,隨便看看就行了。
作者:泰迪的bagwell 連結:https://www.jianshu.com/p/b888a1689822 來源:簡書 簡書著作權歸作者所有,任何形式的轉載都請聯絡作者獲得授權並註明出處。