1. 程式人生 > >JUC原始碼分析-集合篇(九):LinkedBlockingQueue

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相關聯。通過notFullnotEmpty更細膩的控制鎖。

  • 若某執行緒(執行緒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 來源:簡書 簡書著作權歸作者所有,任何形式的轉載都請聯絡作者獲得授權並註明出處。