Java併發包原始碼學習系列:阻塞佇列實現之LinkedBlockingQueue原始碼解析

天喬巴夏丶發表於2021-01-28

系列傳送門:

LinkedBlockingQueue概述

LinkedBlockingQueue是由單連結串列構成的界限可選的阻塞佇列,如不指定邊界,則為Integer.MAX_VALUE,因此如不指定邊界,一般來說,插入的時候都會成功。

LinkedBlockingQueue支援FIFO先進先出的次序對元素進行排序。

類圖結構及重要欄位

public class LinkedBlockingQueue<E> extends AbstractQueue<E>
        implements BlockingQueue<E>, java.io.Serializable {
    private static final long serialVersionUID = -6903933977591709194L;
    // 單連結串列節點
    static class Node<E> {
        E item;
        
        Node<E> next;

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

    /** 容量,如果不指定就是Integer.MAX_VALUE */
    private final int capacity;

    /** 原子變數,記錄元素個數 */
    private final AtomicInteger count = new AtomicInteger();

    /**
     * 哨兵頭節點,head.next才是佇列的第一個元素
     */
    transient Node<E> head;

    /**
     * 指向最後一個元素
     */
    private transient Node<E> last;

    /** 用來控制同時只有一個執行緒可以從隊頭獲取元素 */
    private final ReentrantLock takeLock = new ReentrantLock();

    /** 條件佇列,佇列為空時,執行出隊take操作的執行緒將會被置入該條件佇列 */
    private final Condition notEmpty = takeLock.newCondition();

    /** 用來控制同時只有一個執行緒可以從隊尾插入元素 */
    private final ReentrantLock putLock = new ReentrantLock();

    /** 條件佇列,佇列滿時,執行入隊操作put的執行緒將會被置入該條件佇列 */
    private final Condition notFull = putLock.newCondition();
}
  • 單向連結串列實現,維護head和last兩個Node節點,head是哨兵節點,head.next是第一個真正的元素,last指向隊尾節點。
  • 佇列中的元素通過AtomicInteger型別的原子變數count記錄。
  • 維護兩把鎖:takeLock保證同時只有一個執行緒可以從對頭獲取元素,putLock保證只有一個執行緒可以在隊尾插入元素。
  • 維護兩個條件變數:notEmpty和notFull,維護條件佇列,用以存放入隊出隊阻塞的執行緒。

如果希望獲取一個元素,需要先獲取takeLock鎖,且notEmpty條件成立。

如果希望插入一個元素,需要先獲取putLock鎖,且notFull條件成立。

構造器

使用LinkedBlockingQueue的時候,可以指定容量,也可以使用預設的Integer.MAX_VALUE,幾乎就是無界的了,當然,也可以傳入集合物件,直接構造。

	// 如果不指定容量,預設容量為Integer.MAX_VALUE  (1 << 30) - 1
    public LinkedBlockingQueue() {
        this(Integer.MAX_VALUE);
    }

	// 傳入指定的容量
    public LinkedBlockingQueue(int capacity) {
        if (capacity <= 0) throw new IllegalArgumentException();
        this.capacity = capacity;
        // 初始化last 和 head指標
        last = head = new Node<E>(null);
    }

	// 傳入指定集合物件,容量視為Integer.MAX_VALUE,直接構造queue
    public LinkedBlockingQueue(Collection<? extends E> c) {
        this(Integer.MAX_VALUE);
        // 寫執行緒獲取putLock
        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();
        }
    }

出隊和入隊操作

佇列的操作最核心的部分莫過於入隊和出隊了,後面分析的方法基本上都基於這兩個工具方法。

LinkedBlockingQueue的出隊和入隊相對ArrayBlockingQueue來說就簡單很多啦:

入隊enqueue

    private void enqueue(Node<E> node) {
        // assert putLock.isHeldByCurrentThread();
        // assert last.next == null;
        last = last.next = node;
    }
  1. 將node連線到last的後面。
  2. 更新last指標的位置,指向node。

出隊dequeue

    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; // head向後移一位
        E x = first.item;
        first.item = null;
        return x;
    }

佇列中的元素實際上是從head.first開始的,那麼移除隊頭,其實就是將head指向head.next即可。

阻塞式操作

E take() 阻塞式獲取

take操作將會獲取當前佇列頭部元素並移除,如果佇列為空則阻塞當前執行緒直到佇列不為空,退出阻塞時返回獲取的元素。

如果執行緒在阻塞時被其他執行緒設定了中斷標誌,則丟擲InterruptedException異常並返回。

    public E take() throws InterruptedException {
        E x;
        int c = -1;
        final AtomicInteger count = this.count;
        // 首先要獲取takeLock
        final ReentrantLock takeLock = this.takeLock;
        takeLock.lockInterruptibly();
        try {
            // 如果佇列為空, notEmpty不滿足,就等著
            while (count.get() == 0) {
                notEmpty.await();
            }
            // 出隊
            x = dequeue();
            // c先賦值為count的值, count 減 1
            c = count.getAndDecrement();
            // 這次出隊後至少還有一個元素,喚醒notEmpty中的讀執行緒
            if (c > 1)
                notEmpty.signal();
        } finally {
            takeLock.unlock();
        }
        // c == capacity 表示在該元素出隊之前,佇列是滿的
        if (c == capacity)
            // 因為在這之前佇列是滿的,可能會有寫執行緒在等著,這裡做個喚醒
            signalNotFull();
        return x;
    }

	// 用於喚醒寫執行緒
    private void signalNotFull() {
        final ReentrantLock putLock = this.putLock;
        // 獲取putLock
        putLock.lock();
        try {
            notFull.signal();
        } finally {
            putLock.unlock();
        }
    }

void put(E e) 阻塞式插入

put操作將向隊尾插入元素,如果佇列未滿則插入,如果佇列已滿,則阻塞當前執行緒直到佇列不滿。

如果執行緒在阻塞時被其他執行緒設定了中斷標誌,則丟擲InterruptedException異常並返回。

    public void put(E e) throws InterruptedException {
        if (e == null) throw new NullPointerException();
        // 所有的插入操作 都約定 本地變數c 作為是否失敗的標識
        int c = -1;
        Node<E> node = new Node<E>(e);
        final ReentrantLock putLock = this.putLock;
        final AtomicInteger count = this.count;
        // 插入操作獲取 putLock
        putLock.lockInterruptibly();
        try {
            // 佇列滿,這時notFull條件不滿足,await
            while (count.get() == capacity) {
                notFull.await();
            }
            enqueue(node);
            // c先返回count的值 , 原子變數 + 1 ,
            c = count.getAndIncrement();
            // 至少還有一個空位可以插入,notFull條件是滿足的,喚醒它
            if (c + 1 < capacity)
                notFull.signal();
        } finally {
            putLock.unlock();
        }
        // c == 0 表示在該元素入隊之前,佇列是空的
        if (c == 0)
            // 因為在這之前佇列是空的,可能會有讀執行緒在等著,這裡做個喚醒
            signalNotEmpty();
    }

	// 用於喚醒讀執行緒
    private void signalNotEmpty() {
        final ReentrantLock takeLock = this.takeLock;
        // 獲取takeLock
        takeLock.lock();
        try {
            // 喚醒
            notEmpty.signal();
        } finally {
            takeLock.unlock();
        }
    }

E poll(timeout, unit) 阻塞式超時獲取

在take阻塞式獲取方法的基礎上額外增加超時功能,傳入一個timeout,獲取不到而阻塞的時候,如果時間到了,即使還獲取不到,也只能立即返回null。

    public E poll(long timeout, TimeUnit unit) throws InterruptedException {
        E x = null;
        int c = -1;
        long nanos = unit.toNanos(timeout);
        final AtomicInteger count = this.count;
        final ReentrantLock takeLock = this.takeLock;
        takeLock.lockInterruptibly();
        try {
            // 這裡就是超時機制的邏輯所在
            while (count.get() == 0) {
                if (nanos <= 0)
                    return null;
                nanos = notEmpty.awaitNanos(nanos);
            }
            x = dequeue();
            c = count.getAndDecrement();
            if (c > 1)
                notEmpty.signal();
        } finally {
            takeLock.unlock();
        }
        if (c == capacity)
            signalNotFull();
        return x;
    }

boolean offer(e, timeout, unit) 阻塞式超時插入

在put阻塞式插入方法的基礎上額外增加超時功能,傳入一個timeout,獲取不到而阻塞的時候,如果時間到了,即使還獲取不到,也只能立即返回null。

    public boolean offer(E e, long timeout, TimeUnit unit)
        throws InterruptedException {

        if (e == null) throw new NullPointerException();
        long nanos = unit.toNanos(timeout);
        int c = -1;
        final ReentrantLock putLock = this.putLock;
        final AtomicInteger count = this.count;
        putLock.lockInterruptibly();
        try {
            while (count.get() == capacity) {
                if (nanos <= 0)
                    return false;
                nanos = notFull.awaitNanos(nanos);
            }
            enqueue(new Node<E>(e));
            c = count.getAndIncrement();
            if (c + 1 < capacity)
                notFull.signal();
        } finally {
            putLock.unlock();
        }
        if (c == 0)
            signalNotEmpty();
        return true;
    }

其他常規操作

boolean offer(E e)

offer(E e)是非阻塞的方法,向隊尾插入一個元素,如果佇列未滿,則插入成功並返回true;如果佇列已滿則丟棄當前元素,並返回false。

    public boolean offer(E e) {
        if (e == null) throw new NullPointerException();
        final AtomicInteger count = this.count;
        // 此時佇列已滿,直接返回false
        if (count.get() == capacity)
            return false;
        int c = -1;
        Node<E> node = new Node<E>(e);
        // 插入操作 獲取putLock
        final ReentrantLock putLock = this.putLock;
        putLock.lock();
        try {
            // 加鎖後再校驗一次
            if (count.get() < capacity) {
                enqueue(node);
                c = count.getAndIncrement();
                if (c + 1 < capacity)
                    notFull.signal();
            }
        } finally {
            putLock.unlock();
        }
        if (c == 0)
            signalNotEmpty();
        return c >= 0; // 只要不是-1,就代表成功~
    }

E poll()

從佇列頭部獲取並移除第一個元素,如果佇列為空則返回null。

    public E poll() {
        final AtomicInteger count = this.count;
        if (count.get() == 0)
            return null;
        E x = null;
        int c = -1;
        final ReentrantLock takeLock = this.takeLock;
        takeLock.lock();
        try {
            // 如果佇列不為空,則出隊, 並遞減計數
            if (count.get() > 0) {
                x = dequeue();
                c = count.getAndDecrement();
                if (c > 1)
                    notEmpty.signal();
            }
        } finally {
            takeLock.unlock();
        }
        if (c == capacity)
            signalNotFull();
        return x;
    }

E peek()

瞅一瞅隊頭的元素是啥,如果佇列為空,則返回null。

    public E peek() {
        if (count.get() == 0)
            return null;
        final ReentrantLock takeLock = this.takeLock;
        takeLock.lock();
        try {
            // 實際上第一個元素是head.next
            Node<E> first = head.next;
            if (first == null)
                return null;
            else
                return first.item;
        } finally {
            takeLock.unlock();
        }
    }

Boolean remove(Object o)

移除佇列中與元素o相等【指的是equals方法判定相同】的元素,移除成功返回true,如果佇列為空或沒有匹配元素,則返回false。

    public boolean remove(Object o) {
        if (o == null) return false;
        fullyLock();
        try {
            // trail 和 p 同時向後遍歷, 如果p匹配了,就讓trail.next = p.next代表移除p
            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();
        }
    }

	// trail為p的前驅, 希望移除p節點
    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.
        p.item = null;
        trail.next = p.next;// 移除p
        // 如果p已經是最後一個節點了,就更新一下last
        if (last == p)
            last = trail;
        // 移除一個節點之後,佇列從滿到未滿, 喚醒notFull
        if (count.getAndDecrement() == capacity)
            notFull.signal();
    }
	//----- 多個鎖 獲取和釋放的順序是 相反的

	// 同時上鎖
    void fullyLock() {
        putLock.lock();
        takeLock.lock();
    }
	// 同時解鎖
    void fullyUnlock() {
        takeLock.unlock();
        putLock.unlock();
    }

總結

  • LinkedBlockingQueue是由單連結串列構成的界限可選的阻塞佇列,如不指定邊界,則為Integer.MAX_VALUE,因此如不指定邊界,一般來說,插入的時候都會成功。
  • 維護兩把鎖:takeLock保證同時只有一個執行緒可以從對頭獲取元素,putLock保證只有一個執行緒可以在隊尾插入元素。
  • 維護兩個條件變數:notEmpty和notFull,維護條件佇列,用以存放入隊出隊阻塞的執行緒。

如果希望獲取一個元素,需要先獲取takeLock鎖,且notEmpty條件成立。

如果希望插入一個元素,需要先獲取putLock鎖,且notFull條件成立。

參考閱讀

  • 《Java併發程式設計之美》
  • 《Java併發程式設計的藝術》

相關文章