LinkedBlockingQueue原理分析---基於JDK8

小飛鶴發表於2018-01-26

1.常用的阻塞佇列 

1)ArrayBlockingQueue:規定大小的BlockingQueue,其建構函式必須帶一個int引數來指明其大小.其所含的物件是以FIFO(先入先出)順序排序的.

2)LinkedBlockingQueue:大小不定的BlockingQueue,若其建構函式帶一個規定大小的引數,生成的BlockingQueue有大小限制,若不帶大小引數,所生成的BlockingQueue的大小由Integer.MAX_VALUE來決定.其所含的物件是以FIFO(先入先出)順序排序的

3)PriorityBlockingQueue:類似於LinkedBlockQueue,但其所含物件的排序不是FIFO,而是依據物件的自然排序順序或者是建構函式的Comparator決定的順序.

4)SynchronousQueue:特殊的BlockingQueue,對其的操作必須是放和取交替完成的.

其中LinkedBlockingQueue和ArrayBlockingQueue比較起來,它們背後所用的資料結構不一樣,導致LinkedBlockingQueue的資料吞吐量要大於ArrayBlockingQueue,但線上程數量很大時其效能的可預見性低於ArrayBlockingQueue


2.LinkedBlockingQueue原理

  1. 基於連結串列實現,執行緒安全的阻塞佇列。
  2. 使用鎖分離方式提高併發,雙鎖(ReentrantLock):takeLock、putLock,允許讀寫並行,remove(e)和contain()、clear()需要同時獲取2個鎖。
  3. FIFO先進先出模式。
  4. 在大部分併發場景下,LinkedBlockingQueue的吞吐量比ArrayBlockingQueue更好,雙鎖,入隊和出隊同時進行
  5. 根據構造傳入的容量大小決定有界還是無界,預設不傳的話,大小Integer.Max


3.LinkedBlockingQueue的幾個關鍵屬性

static class Node<E> {
        E item;

        /**後繼節點
         */
        Node<E> next;

        Node(E x) { item = x; }
    }

    /** 佇列容量,預設最大,可指定大小 */
    private final int capacity;

    /** 當前容量 */
    private final AtomicInteger count = new AtomicInteger();

    /**
     * 頭節點.
     * Invariant: head.item == null
     */
    transient Node<E> head;

    /**
     * 尾節點.
     * Invariant: last.next == null
     */
    private transient Node<E> last;

    /** 定義的出隊和入隊分離鎖,2個佇列空和滿的出隊和入隊條件 Lock held by take, poll, etc */
    private final ReentrantLock takeLock = new ReentrantLock();

    /** Wait queue for waiting takes */
    private final Condition notEmpty = takeLock.newCondition();

    /** Lock held by put, offer, etc */
    private final ReentrantLock putLock = new ReentrantLock();

    /** Wait queue for waiting puts */
    private final Condition notFull = putLock.newCondition();

建構函式:預設是佇列,可指定為有界,或初始給於一個初始集合資料


public LinkedBlockingQueue() {
        this(Integer.MAX_VALUE);
    }

    /**
     * 指定有界大小,同時初始化head和tail節點
     *
     * @param capacity the capacity of this queue
     * @throws IllegalArgumentException if {@code capacity} is not greater
     *         than zero
     */
    public LinkedBlockingQueue(int capacity) {
        if (capacity <= 0) throw new IllegalArgumentException();
        this.capacity = capacity;
        last = head = new Node<E>(null);
    }

    /**
     * 遍歷集合元素,放到佇列進行初始化  ---  無界佇列
     *
     * @param c the collection of elements to initially contain
     * @throws NullPointerException if the specified collection or any
     *         of its elements are null
     */
    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) {
                if (e == null)
                    throw new NullPointerException();
                if (n == capacity)
                    throw new IllegalStateException("Queue full");
                enqueue(new Node<E>(e));
                ++n;
            }
            count.set(n);
        } finally {
            putLock.unlock();
        }
    }

4.BlockingQueue原始碼分析

    //入隊,將元素新增到對尾等價  last.next = node; last = last.next
    private void enqueue(Node<E> node) {
        last = last.next = node;
    }

    /**
     * 出隊,從頭部出
     *
     * @return the node
     */
    private E dequeue() {
        // assert takeLock.isHeldByCurrentThread();
        // assert head.item == null;
        Node<E> h = head;
        Node<E> first = h.next;
        h.next = h; // help GC
        head = first;
        E x = first.item;
        first.item = null;
        return x;
    }


// 佇列已滿:false  
public boolean offer(E e) {  
    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(); // 獲取插入鎖putLock  
    try {  
        if (count.get() < capacity) { // 加鎖後再次判斷佇列是否已滿  
            enqueue(node); // 入隊  
            c = count.getAndIncrement(); // 返回Inc之前的值  
            if (c + 1 < capacity) // 插入節點後佇列未滿  
                notFull.signal(); // 喚醒notFull上的等待執行緒  
        }  
    } finally {  
        putLock.unlock(); // 釋放插入鎖  
    }  
    if (c == 0)  
        signalNotEmpty(); // 如果offer前佇列為空,則喚醒notEmpty上的等待執行緒  
    return c >= 0;  
}  



public boolean offer(E e, long timeout, TimeUnit unit) throws InterruptedException 方法和offer(E e)程式碼和功能均相似,但是如果在指定時間內未插入成功則會返回false。
比offer(E e)多的部分程式碼分析:
long nanos = unit.toNanos(timeout);  //將指定的時間長度轉換為毫秒來進行處理  
while (count.get() == capacity) {  
    if (nanos <= 0) // 等待的剩餘時間小於等於0,那麼直接返回false  
        return false;  
    nanos = notFull.awaitNanos(nanos); // 最多等待時間(納秒)  
}  


//插入節點:\n執行緒入隊操作前會獲取putLock鎖,插入資料完畢後釋放;
佇列未滿將新建Node節點,新增到佇列末尾;
佇列已滿則阻塞執行緒(notFull.await())或返回false;若執行緒B取出資料,則會呼叫notFull.signal()喚醒notFull上的等待執行緒(執行緒A繼續插資料)。
若入隊前佇列為空,則喚醒notEmpty上等待的獲取資料的執行緒
// 一直阻塞直到插入成功  
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;  
 // 可中斷的鎖獲取操作(優先考慮響應中斷),如果執行緒由於獲取鎖而處於Blocked狀態時,執行緒將被中斷而不再繼續等待(throws InterruptedException),可避免死鎖。  
    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迴圈可避免“偽喚醒”(執行緒被喚醒時佇列大小依舊達到最大值)  
        while (count.get() == capacity) {  
            notFull.await(); // notFull:入隊條件  
        }  
        enqueue(node); // 將node連結到佇列尾部  
        c = count.getAndIncrement(); // 元素入隊後佇列元素總和  
        if (c + 1 < capacity) // 佇列未滿  
            notFull.signal(); // 喚醒其他執行入佇列的執行緒  
    } finally {  
        putLock.unlock(); // 釋放鎖  
    }  
// c=0說明佇列之前為空,出佇列執行緒均處於等待狀態。新增一個元素後,佇列已不為空,於是喚醒等待獲取元素的執行緒  
    if (c == 0)  
        signalNotEmpty();  
}  


獲取方法
先看幾個重要方法:
  1. /** 
  2.  * 喚醒等待插入資料的執行緒. Called only from take/poll. 
  3.  */  
  4. private void signalNotFull() {  
  5.     final ReentrantLock putLock = this.putLock;  
  6.     putLock.lock();  
  7.     try {  
  8.         notFull.signal();  
  9.     } finally {  
  10.         putLock.unlock();  
  11.     }  
  12. }  
  13. /** 
  14. * 佇列頭部元素出隊. 
  15. * 
  16. * @return the node 
  17. */  
  18. private E dequeue() {  
  19.     // assert takeLock.isHeldByCurrentThread();  
  20.     // assert head.item == null;  
  21.     Node<E> h = head; // 臨時變數h  
  22.     Node<E> first = h.next;  
  23.     h.next = h; // 形成環引用help GC  
  24.     head = first;  
  25.     E x = first.item;  
  26.     first.item = null;  
  27.     return x;  
  28. }  

4.1、poll()
  1. // 佇列為空返回null而不是拋異常  
  2. public E poll() {  
  3.     final AtomicInteger count = this.count;  
  4.     if (count.get() == 0)  
  5.         return null;  
  6.     E x = null;  
  7.     int c = -1;  
  8.     final ReentrantLock takeLock = this.takeLock;  
  9.     takeLock.lock();  
  10.     try {  
  11.         if (count.get() > 0) {  
  12.             x = dequeue();  
  13.             c = count.getAndDecrement(); // 減1並返回舊值  
  14.             if (c > 1)  
  15.                 notEmpty.signal(); // 喚醒其他取資料的執行緒  
  16.         }  
  17.     } finally {  
  18.         takeLock.unlock();  
  19.     }  
  20.  // c等於capacity說明poll之前佇列已滿,poll一個元素後便可喚醒其他等待插入資料的執行緒  
  21.     if (c == capacity)  
  22.         signalNotFull();  
  23.     return x;  
  24. }  

衍生方法:
// 為poll方法增加了時間限制,指定時間未取回資料則返回null
  1. public E poll(long timeout, TimeUnit unit)throws InterruptedException{}  

4.2、take()
// 一直阻塞直到取回資料
  1. public E take() throws InterruptedException {  
  2.     E x;  
  3.     int c = -1;  
  4.     final AtomicInteger count = this.count;  
  5.     final ReentrantLock takeLock = this.takeLock;  
  6.     takeLock.lockInterruptibly();  
  7.     try {  
  8.         while (count.get() == 0) { // 佇列為空,一直等待  
  9.             notEmpty.await();  
  10.         }  
  11.         x = dequeue(); // 出隊  
  12.         c = count.getAndDecrement();  
  13.         if (c > 1// take資料前佇列大小大於1,則take後佇列至少還有1個元素  
  14.             notEmpty.signal(); // 喚醒其他取資料的執行緒  
  15.     } finally {  
  16.         takeLock.unlock();  
  17.     }  
  18.     if (c == capacity)  
  19.         signalNotFull(); //喚醒其他等待插入資料的執行緒  
  20.     return x;  
  21. }  

4.3、drainTo(Collection<? super E> c, int maxElements)
// 移除最多maxElements個元素並將其加入集合
  1. public int drainTo(Collection<? super E> c, int maxElements) {  
  2.     if (c == null)  
  3.         throw new NullPointerException();  
  4.     if (c == this)  
  5.         throw new IllegalArgumentException();  
  6.     if (maxElements <= 0)  
  7.         return 0;  
  8.     boolean signalNotFull = false;  
  9.     final ReentrantLock takeLock = this.takeLock;  
  10.     takeLock.lock();  
  11.     try {  
  12.         int n = Math.min(maxElements, count.get());//轉移元素數量不能超過佇列總量   
  13.         // count.get provides visibility to first n Nodes  
  14.         Node<E> h = head;  
  15.         int i = 0;  
  16.         try {  
  17.             while (i < n) {  
  18.                 Node<E> p = h.next;//從隊首獲取元素  
  19.                 c.add(p.item);  
  20.                 p.item = null;//p為臨時變數,置null方便GC  
  21.                 h.next = h;  
  22.                 h = p;  
  23.                 ++i;  
  24.             }  
  25.             return n;  
  26.         } finally {  
  27.             // Restore invariants even if c.add() threw  
  28.             if (i > 0) { // 有資料被轉移到集合c中  
  29.                 // assert h.item == null;  
  30.                 head = h;  
  31.  //如果轉移前的佇列大小等於佇列容量,則說明現在佇列未滿  
  32.  // 更新count為佇列實際大小(減去i得到)  
  33.                 signalNotFull = (count.getAndAdd(-i) == capacity);  
  34.             }  
  35.         }  
  36.     } finally {  
  37.         takeLock.unlock();  
  38.         if (signalNotFull)  
  39.             signalNotFull(); // 喚醒其他等待插入資料的執行緒  
  40.     }  
  41. }  

衍生方法:
// 將[所有]可用元素加入集合c
  1.  public int drainTo(Collection<? super E> c) {  
  2.     return drainTo(c, Integer.MAX_VALUE);  
  3. }  

4.4、boolean retainAll(Collection<?> c)
// 僅保留集合c中包含的元素,佇列因此請求而改變則返回true
  1. public boolean retainAll(Collection<?> c) {  
  2.     Objects.requireNonNull(c); // 集合為null則throw NPE  
  3.     boolean modified = false;  
  4.     Iterator<E> it = iterator();  
  5.     while (it.hasNext()) {  
  6.         if (!c.contains(it.next())) {  
  7.             it.remove();  
  8.             modified = true// 佇列因此請求而改變則返回true  
  9.         }  
  10.     }  
  11.     return modified;  
  12. }  

LinkedBlockingQueue取資料小結:
執行緒A取資料前會獲取takeLock鎖,取完資料後釋放鎖。
佇列有資料則(通常)返回隊首資料;
若佇列為空,則阻塞執行緒(notEmpty.await())或返回null等;當執行緒B插入資料後,會呼叫notEmpty.signal()喚醒notEmpty上的等待執行緒(執行緒A繼續取資料)。
若取資料前佇列已滿,則通過notFull.signal()喚醒notFull上等待插入資料的執行緒。

5、檢測方法(取回但不移除)
5.1、E peek()
// 返回佇列頭,佇列為空返回null
  1. public E peek() {  
  2.     if (count.get() == 0)  
  3.         return null;  
  4.     final ReentrantLock takeLock = this.takeLock;  
  5.     takeLock.lock();  
  6.     try {  
  7.         Node<E> first = head.next;  
  8.         if (first == null)  
  9.             return null;  
  10.         else  
  11.             return first.item;  
  12.     } finally {  
  13.         takeLock.unlock();  
  14.     }  
  15. }  

6、綜述
6.1、LinkedBlockingQueue通過對 插入、取出資料 使用不同的鎖,實現多執行緒對競爭資源的互斥訪問

6.2、(之前佇列為空)新增資料後呼叫signalNotEmpty()方法喚醒等待取資料的執行緒;(之前佇列已滿)取資料後呼叫signalNotFull()喚醒等待插入資料的執行緒。這種喚醒模式可節省執行緒等待時間。

6.3、個別操作需要呼叫方法fullyLock()同時獲取putLock、takeLock兩把鎖(如方法:clear()、contains(Object o)、remove(Object o)、toArray()、toArray(T[] a)、toString()),注意fullyLock和fullyUnlock獲取鎖和解鎖的順序剛好相反,避免死鎖。
  1. /** 
  2.  * Locks to prevent both puts and takes. 
  3.  */  
  4. void fullyLock() {  
  5.     putLock.lock();  
  6.     takeLock.lock();  
  7. }  
  8. /** 
  9.  * Unlocks to allow both puts and takes. 
  10.  */  
  11. void fullyUnlock() {  
  12.     takeLock.unlock();  
  13.     putLock.unlock();  
  14. }  

6.4、執行緒喚醒signal()
值得注意的是,對notEmpty和notFull的喚醒操作均使用的是signal()而不是signalAll()。
signalAll() 雖然能喚醒Condition上所有等待的執行緒,但卻並不見得會節省資源,相反,喚醒操作會帶來上下文切換,且會有鎖的競爭。此外,由於此處獲取的鎖均是同一個(putLock或takeLock),同一時刻被鎖的執行緒只有一個,也就無從談起喚醒多個執行緒了。

6.5、LinkedBlockingQueue與ArrayBlockingQueue簡要比較
ArrayBlockingQueue底層基於陣列,建立時必須指定佇列大小,“有界”;LinkedBlockingQueue“無界”,節點動態建立,節點出隊後可被GC,故伸縮性較好;
ArrayBlockingQueue入隊和出隊使用同一個lock(但資料讀寫操作已非常簡潔),讀取和寫入操作無法並行,LinkedBlockingQueue使用雙鎖可並行讀寫,其吞吐量更高。
ArrayBlockingQueue在插入或刪除元素時直接放入陣列指定位置(putIndex、takeIndex),不會產生或銷燬任何額外的物件例項;而LinkedBlockingQueue則會生成一個額外的Node物件,在高效併發處理大量資料時,對GC的影響存在一定的區別。

參考、感謝:

http://blog.csdn.net/u010887744/article/details/73010691







相關文章