【JUC】JDK1.8原始碼分析之LinkedBlockingQueue(四)

leesf發表於2016-05-29

一、前言

  分析完了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 類的屬性  

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();
}
View Code

  說明:可以看到LinkedBlockingQueue包含了讀、寫重入鎖(與ArrayBlockingQueue不同,ArrayBlockingQueue只包含了一把重入鎖),讀寫操作進行了分離,並且不同的鎖有不同的Condition條件(與ArrayBlockingQueue不同,ArrayBlockingQueue是一把重入鎖的兩個條件)。

  3.4 類的建構函式

  1. LinkedBlockingQueue()型建構函式  

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

  說明:該建構函式用於建立一個容量為 Integer.MAX_VALUE 的 LinkedBlockingQueue。

  2. LinkedBlockingQueue(int)型建構函式  

    public LinkedBlockingQueue(int capacity) {
        // 初始化容量必須大於0
        if (capacity <= 0) throw new IllegalArgumentException();
        // 初始化容量
        this.capacity = capacity;
        // 初始化頭結點和尾結點
        last = head = new Node<E>(null);
    }
View Code

  說明:該建構函式用於建立一個具有給定(固定)容量的 LinkedBlockingQueue。

  3. LinkedBlockingQueue(Collection<? extends E>)型建構函式 

    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();
        }
    }
View Code

  說明:該建構函式用於建立一個容量是 Integer.MAX_VALUE 的 LinkedBlockingQueue,最初包含給定 collection 的元素,元素按該 collection 迭代器的遍歷順序新增。

  3.5 核心函式分析

  1. put函式  

    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();
    }
View Code

  說明:put函式用於存放元素,其流程如下。

  ① 判斷元素是否為null,若是,則丟擲異常,否則,進入步驟②

  ② 獲取存元素鎖,並上鎖,如果當前執行緒被中斷,則丟擲異常,否則,進入步驟③

  ③ 判斷當前佇列中的元素個數是否已經達到指定容量,若是,則在notFull條件上進行等待,否則,進入步驟④

  ④ 將新生結點入佇列,更新佇列元素個數,若元素個數小於指定容量,則喚醒在notFull條件上等待的執行緒,表示可以繼續存放元素。進入步驟⑤

  ⑤ 釋放鎖,判斷結點入佇列之前的元素個數是否為0,若是,則喚醒在notEmpty條件上等待的執行緒(表示佇列中沒有元素,取元素執行緒被阻塞了)。

  put函式中會呼叫到enqueue函式和signalNotEmpty函式,enqueue函式原始碼如下  

    private void enqueue(Node<E> node) {
        // assert putLock.isHeldByCurrentThread();
        // assert last.next == null;
        // 更新尾結點域
        last = last.next = node;
    }
View Code

  說明:可以看到,enqueue函式只是更新了尾節點。signalNotEmpty函式原始碼如下 

    private void signalNotEmpty() {
        // 取元素鎖
        final ReentrantLock takeLock = this.takeLock;
        // 獲取鎖
        takeLock.lock();
        try {
            // 喚醒在notEmpty條件上等待的某個執行緒
            notEmpty.signal();
        } finally {
            // 釋放鎖
            takeLock.unlock();
        }
    }
View Code

  說明:signalNotEmpty函式用於喚醒在notEmpty條件上等待的執行緒,其首先獲取取元素鎖,然後上鎖,然後喚醒在notEmpty條件上等待的執行緒,最後釋放取元素鎖。

  2. offer函式 

    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;
    }
View Code

  說明:offer函式也用於存放元素,offer函式新增元素不會丟擲異常(其他的域put函式類似)。

  3. 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) { // 元素個數為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;
    }
View Code

  說明:take函式用於獲取一個元素,其與put函式相對應,其流程如下。

  ① 獲取取元素鎖,並上鎖,如果當前執行緒被中斷,則丟擲異常,否則,進入步驟②

  ② 判斷當前佇列中的元素個數是否為0,若是,則在notEmpty條件上進行等待,否則,進入步驟③

  ③ 出佇列,更新佇列元素個數,若元素個數大於1,則喚醒在notEmpty條件上等待的執行緒,表示可以繼續取元素。進入步驟④

  ④ 釋放鎖,判斷結點出佇列之前的元素個數是否為指定容量,若是,則喚醒在notFull條件上等待的執行緒(表示佇列已滿,存元素執行緒被阻塞了)。

  take函式呼叫到了dequeue函式和signalNotFull函式,dequeue函式原始碼如下  

    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;
    }
View Code

  說明:dequeue函式的作用是將頭結點更新為之前頭結點的下一個結點,並且將更新後的頭結點的item域設定為null。signalNotFull函式的原始碼如下

    private void signalNotFull() {
        // 存元素鎖
        final ReentrantLock putLock = this.putLock;
        // 獲取鎖
        putLock.lock();
        try {
            // 喚醒在notFull條件上等待的某個執行緒
            notFull.signal();
        } finally {
            // 釋放鎖
            putLock.unlock();
        }
    }
View Code

  說明:signalNotFull函式用於喚醒在notFull條件上等待的某個執行緒,其首先獲取存元素鎖,然後上鎖,然後喚醒在notFull條件上等待的執行緒,最後釋放存元素鎖。

  4. poll函式  

    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;
    }
View Code

  說明:poll函式也用於存放元素,poll函式新增元素不會丟擲異常(其他的與take函式類似)。

  5. remove函式  

    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();
        }
    }
View Code

  說明:remove函式的流程如下

  ① 獲取讀、寫鎖(防止此時繼續出、入佇列)。進入步驟②

  ② 遍歷連結串列,尋找指定元素,若找到,則將該結點從連結串列中斷開,有利於被GC,進入步驟③

  ③ 釋放讀、寫鎖(可以繼續出、入佇列)。步驟②中找到指定元素則返回true,否則,返回false。

  其中,remove函式會呼叫unlink函式,其原始碼如下  

    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();
    }
View Code

  說明:unlink函式用於將指定結點從連結串列中斷開,並且更新佇列元素個數,並且判斷若之前佇列元素的個數達到了指定容量,則會喚醒在notFull條件上等待的某個執行緒。

四、示例

  下面通過一個示例來了解LinkedBlockingQueue的使用。  

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();
    }
}
View Code

  執行結果:  

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
View Code

  說明:示例中使用了兩個執行緒,一個用於存元素,一個用於讀元素,存和讀各10次,每個執行緒存一個元素或者讀一個元素後都會休眠100ms,可以看到結果是交替打 印,並且首先列印的肯定是put執行緒語句(因為若取執行緒先取元素,此時佇列並沒有元素,其會阻塞,等待存執行緒存入元素),並且最終程式可以正常結束。

  ① 若修改取元素執行緒,將存的元素的次數修改為15次(for迴圈的結束條件改為15即可),執行結果如下:  

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
View Code

  說明:執行結果與上面的執行結果相同,但是,此時程式無法正常結束,因為take方法被阻塞了,等待被喚醒。

五、總結

  LinkedBlockingQueue的原始碼相對比較簡單,其也是通過ReentrantLock和Condition條件來保證多執行緒的正確訪問的,並且取元素(出佇列)和存元素(入佇列)是採用不同的鎖,進行了讀寫分離,有利於提高併發度。LinkedBockingQueue的分析就到這裡,歡迎交流,謝謝各位園友的觀看~

相關文章