深度解析阻塞隊列LinkedBlockingQueue

前言

關於阻塞隊列的使用,其實之前的文章已經提到過:三種方式實現生產者-消費者模型,最後一種方式就是用阻塞隊列實現的。仔細觀察會發現,前兩種也是在用wait/notifyReentrantLock/Condition模擬阻塞隊列。
本篇主要從源碼、核心方法、設計思想等方面全面解析LinkedBlockingQueue

有本篇涉及到單鏈表的操作以及ReentrantLockCondition的使用,所以提前瞭解一下可以做到事半功倍的效果。

阻塞隊列

LinkedBlockingQueue是阻塞隊列中的一種,見名知意,由鏈表實現的阻塞隊列。類的繼承結構圖如下:
LinkedBlockingQueue繼承結構圖
可以看到LinkedBlockingQueue實現了BlockingQueue,相當於實現了Queue接口。因爲阻塞隊列也滿足FIFO(先入先出)特性,能操作的只有隊列的頭部和尾部,所以相對來說,LinkedBlockingQueue接口對外提供的方法比較少,主要是入隊和出隊操作。

內部結構

要想知道LinkedBlockingQueue的內部結構,得先了解類的定義和成員變量/常量。

public class LinkedBlockingQueue<E> extends AbstractQueue<E>
        implements BlockingQueue<E>, java.io.Serializable {
    private static final long serialVersionUID = -6903933977591709194L;

    /**
     * Linked list node class
     * 鏈表內部節點定義
     * 根據節點的定義可以看出LinkedBlockingQueue由單鏈表實現的
     */
    static class Node<E> {
    	// 數據域
        E item;
		// 指針域
        Node<E> next;

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

    /** The capacity bound, or Integer.MAX_VALUE if none */
    // 隊列容量,不指定的話就是Integer.MAX_VALUE,也就是無界隊列
    private final int capacity;

    /** Current number of elements */
    // 隊列中元素的個數
    private final AtomicInteger count = new AtomicInteger();

    /**
     * Head of linked list.
     * Invariant: head.item == null
     * 隊列(鏈表)頭節點
     */
    transient Node<E> head;

    /**
     * Tail of linked list.
     * Invariant: last.next == null
     * 隊列(鏈表)尾節點
     */
    private transient Node<E> last;

    /** 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();
}    

從類的定義及成員變量來看,基本上可以猜測LinkedBlockingQueue底層數據結構是單鏈表,存/取元素通過ReentrantLock + Condition來保證線程安全以及實現線程阻塞。

構造方法

LinkedBlockingQueue構造方法有三個:

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

public LinkedBlockingQueue(int capacity) {
    if (capacity <= 0) throw new IllegalArgumentException();
    this.capacity = capacity;
    // 初始化時,頭指針和尾指針都指向同一個空節點
    last = head = new Node<E>(null);
}

public LinkedBlockingQueue(Collection<? extends E> c) {
    this(Integer.MAX_VALUE);
    final ReentrantLock putLock = this.putLock;
    // 加鎖(put鎖)
    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();
    }
}

構造方法比較簡單,只需要關注一點:如果不指定隊列容量,默認就是Integer.MAX_VALUE,無界隊列。

核心方法

隊列的核心操作只有三個:入隊、出隊、查看隊首元素。LinkedBlockingQueue的入隊、出隊操作對應實現了三組API。分別在隊列滿和隊列空時有不同的表現。
三組API對應的表現如下:

(隊列滿或者空)拋異常 (隊列滿或者空)返回特殊值 (隊列滿或者空)阻塞
入隊 add拋出異常 offer 返回false put
出隊 remove拋出異常 poll返回null take
查看隊首元素 element拋出異常 peek返回null

add/offer/put三個方法都不支持傳入null,會拋出空指針異常。
對於三個拋出異常的方法add/remove/element都在父類AbstractQueue中實現,分別是offer/poll/peek方法的語法糖。

offer(E e)方法

offer(E e)方法用於入隊操作,其源碼如下

public boolean offer(E e) {
	// 不能放入null
    if (e == null) throw new NullPointerException();
    // 隊列中元素個數
    final AtomicInteger count = this.count;
    if (count.get() == capacity)
    	// 元素個數等於隊列容量,隊列已滿,返回特殊值false
        return false;
    int c = -1;
    Node<E> node = new Node<E>(e);
    final ReentrantLock putLock = this.putLock;
    // 加鎖(put鎖)
    putLock.lock();
    try {
        if (count.get() < capacity) {
        	// 雙重校驗,隊列此時未滿,入隊
            enqueue(node);
            // count++
            c = count.getAndIncrement();
            if (c + 1 < capacity)
            	// 入隊一個元素後,隊列依然未滿,喚醒因爲調用put方法而被阻塞的線程
                notFull.signal();
        }
    } finally {
    	// 解鎖
        putLock.unlock();
    }
    if (c == 0)
    	// 隊列中有一個元素,喚醒因爲調用take方法而被阻塞的線程
        signalNotEmpty();
    return c >= 0;
}

瞭解了整體執行邏輯,再具體看看入隊方法enqueue,其源碼如下:

private void enqueue(Node<E> node) {
    // assert putLock.isHeldByCurrentThread();
    // assert last.next == null;
    // 節點插在鏈表尾部,並把尾指針指向新插入的節點
    last = last.next = node;
}

因爲有尾指針的存在,所以在單鏈表的尾部插入元素的時間複雜度是O(1),非常高效。
LinkedBlockingQueue還提供了一個重載的offer方法offer(E e, long timeout, TimeUnit unit),執行邏輯差不多,只是加了超時時間。

poll()方法

poll()方法用於出隊操作,從隊列頭部取元素。其源碼如下:

public E poll() {
	// 獲取隊列元素個數
    final AtomicInteger count = this.count;
    if (count.get() == 0)
    	// 隊列爲空(沒有元素),返回特殊值null
        return null;
    // 要返回的結果
    E x = null;
    int c = -1;
    final ReentrantLock takeLock = this.takeLock;
    // 加鎖(take鎖,和put不是同一把鎖)
    takeLock.lock();
    try {
        if (count.get() > 0) {
        	// 雙重校驗,隊列中有元素,執行出隊操作
            x = dequeue();
            // count--
            c = count.getAndDecrement();
            if (c > 1)
            	// 隊列中還有元素,喚醒因爲調用take方法而被阻塞的線程
                notEmpty.signal();
        }
    } finally {
    	// 解鎖
        takeLock.unlock();
    }
    if (c == capacity)
    	// 隊列還能入隊一個元素,喚醒因爲調用put方法而被阻塞的線程
        signalNotFull();
    // 返回出隊的元素
    return x;
}

瞭解整體邏輯之後再來具體看下出隊方法dequeue,其源代碼定義如下:

private E dequeue() {
    // assert takeLock.isHeldByCurrentThread();
    // assert head.item == null;
    // head的數據域爲null,相當於虛擬頭節點,next指向的纔是隊列真正的第一個元素
    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;
}

出隊操作是從單鏈表的頭部刪除一個節點,時間複雜度也是O(1),可以看出入隊出隊操作都是非常高效的。
LinkedBlockingQueue也提供了一個重載的poll方法E poll(long timeout, TimeUnit unit)
offer/poll只是現場安全的實現了隊列的基本操作,光有這組操作的隊列不能算是阻塞隊列,所以再來看看入隊、出隊的阻塞式實現

put(E e)方法

put(E e)方法和offer(E e)方法一樣,都是往隊列尾部插入元素(入隊)。不同的是,當隊列滿了的時候,put方法會阻塞當前線程,直到有線程從隊列中取出元素,隊列還有剩餘空間的時候纔會繼續進行入隊操作;而offer方法直接返回特殊值false。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;
    // 加鎖(put鎖),lockInterruptibly是可中斷鎖
    putLock.lockInterruptibly();
    try {
        while (count.get() == capacity) {
        	// 隊列已滿,阻塞當前線程
            notFull.await();
        }
        // 隊列未滿,入隊
        enqueue(node);
        // count++
        c = count.getAndIncrement();
        if (c + 1 < capacity)
        	// 入隊一個元素後,隊列依然未滿,喚醒因調用put方法而阻塞的線程
            notFull.signal();
    } finally {
    	// 解鎖
        putLock.unlock();
    }
    if (c == 0)
    	// 隊列中還有一個元素,喚醒因調用take方法而被阻塞的線程
        signalNotEmpty();
}

可以看到整體邏輯是用ReentrantLock鎖 + Condition實現:隊列滿時,阻塞當前線程,直到隊列非滿。

E take()方法

E take()方法和poll()方法一樣,都是刪除並返回隊列頭部元素(出隊)。不同的是,當隊列爲空的時候,take方法會阻塞當前線程,直到有線程往隊列中放入元素,纔會繼續進行出隊操作;而poll方法直接返回特殊值null。take方法的源代碼如下:

public E take() throws InterruptedException {
	// 要返回的元素
    E x;
    int c = -1;
    // 隊列中元素的個數
    final AtomicInteger count = this.count;
    final ReentrantLock takeLock = this.takeLock;
    // 加鎖(take鎖,不同於put鎖),lockInterruptibly是可中斷鎖
    takeLock.lockInterruptibly();
    try {
        while (count.get() == 0) {
        	// 隊列爲空,阻塞當前線程
            notEmpty.await();
        }
        // 隊列非空,直接出隊
        x = dequeue();
        // count--
        c = count.getAndDecrement();
        if (c > 1)
        	// 隊列中還有元素,喚醒因爲調用take方法而被阻塞的線程
            notEmpty.signal();
    } finally {
    	// 解鎖
        takeLock.unlock();
    }
    if (c == capacity)
    	// 隊列中還能入隊一個元素,喚醒因爲調用put方法而被阻塞的線程
        signalNotFull();
    // 返回出隊的元素
    return x;
}

可以看到整體邏輯是用ReentrantLock鎖 + Condition實現:隊列爲空時,阻塞當前線程,直到隊列非空。

peek()方法

除了入隊和出隊,隊列還有一個基本操作就是查看隊首元素(和出隊操作的區別是:是否刪除隊首元素),方法名是peek,源代碼如下:

public E peek() {
    if (count.get() == 0)
    	// 隊列爲空,返回特殊值null
        return null;
    final ReentrantLock takeLock = this.takeLock;
    // 加鎖
    takeLock.lock();
    try {
    	// 取隊列頭部元素
    	// head相當於虛擬頭節點,head.next纔是隊列真正的第一個元素
        Node<E> first = head.next;
        if (first == null)
            return null;
        else
            return first.item;
    } finally {
    	// 解鎖
        takeLock.unlock();
    }
}

基本是隻是一個讀操作,但是也加鎖了。是爲了防止在讀的過程中,有線程執行了出隊操作。

總結

總的來說LinkedBlockingQueue可以總結出以下特點:

  • 底層用帶有頭指針和尾指針的單鏈表實現,入隊/出隊都非常高效
  • ReentrantLock鎖實現入隊、出隊、查看隊首元素等操作的線程安全
  • 用兩把ReentrantLock操作入隊、出隊線程,使出隊和入隊可以同時進行,併發度更高
  • Condition類實現有條件的分組喚醒
  • 入隊操作是把元素插在鏈表尾部,出隊操作是把鏈表頭部元素刪除並返回

LinkedBlockingQueue使用非常廣泛,線程池中的緩衝隊列,生產者/消費者模型的實現都離不開它。

發佈了55 篇原創文章 · 獲贊 107 · 訪問量 1萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章