BlockingQueue介面及其實現類的原始碼分析

pb_yan發表於2018-06-11

      BlockingQueue是一個阻塞佇列的介面,提供了一系列的介面方法。其中方法主要可以分為三類,包括Insert相關的add、offer、put,remove相關的remove()、poll()、take()方法,以及檢視相關的peek()、element方法等。阻塞佇列是執行緒安全的容器,其元素不允許為null,否則會丟擲空指標異常。阻塞佇列可以用於生產者消費者場景中去。

      常見的實現類有ArrayBlockingQueue以及LinkedBlockingQueue兩種,下面來詳細來看一下ArrayBlockingQueue以及LinkedBlockingQueue的具體實現。

  ArrayBlockingQueue重要引數及建構函式

  /** 佇列中元素的資料結構 */
    final Object[] items;

    /** 隊首標記,用於出隊操作 */
    int takeIndex;

    /** 隊尾標記,用於入隊操作 */
    int putIndex;

    /** 佇列中元素的個數 */
    int count;

    /*
     * Concurrency control uses the classic two-condition algorithm
     * found in any textbook.
     */

    /**對於任何操作都需要加鎖,這裡是一個可重入鎖 */
    final ReentrantLock lock;

    /** 用於通知取元素的條件 */
    private final Condition notEmpty;

    /** 控制放元素的條件 */
    private final Condition notFull;
  /**
     * 創造一個給定容量的非公平鎖的阻塞佇列
     */
    public ArrayBlockingQueue(int capacity) {
        this(capacity, false);
    }

    /**
     * 創造一個給定容量的陣列,並根據是否公平建立相應的公平鎖。初始化條件。
     */
    public ArrayBlockingQueue(int capacity, boolean fair) {
        if (capacity <= 0)
            throw new IllegalArgumentException();
        this.items = new Object[capacity];
        lock = new ReentrantLock(fair);
        notEmpty = lock.newCondition();
        notFull =  lock.newCondition();
    }

ArrayBlockingQueue入隊操作

      以常用的add方法為例子,在這個方法內部實際上是呼叫了offer方法,而offer方法在加鎖的基礎之上呼叫了enqueue方法來將元素放在陣列的尾部,並喚醒那些阻塞了的取元素方法。

add、offer、put的主要區別如下:

add方法在成功是返回true,如果失敗就丟擲異常。

offer方法在新增成功返回true,新增失敗返回false。

put方法如果新增不成功就會一直等待。不會出現丟節點的情況一般。

 /**
     * 將元素插入佇列中,如果成功則返回true,否則的話丟擲異常。可以看出其本身還是呼叫了offer方法。
     *
     * <p>This implementation returns <tt>true</tt> if <tt>offer</tt> succeeds,
     * else throws an <tt>IllegalStateException</tt>.
     *
     *  
     */
    public boolean add(E e) {
        if (offer(e))
            return true;
        else
            throw new IllegalStateException("Queue full");
    }
 /**
     * Inserts the specified element at the tail of this queue if it is
     * possible to do so immediately without exceeding the queue's capacity,
     * returning {@code true} upon success and {@code false} if this queue
     * is full.  This method is generally preferable to method {@link #add},
     * which can fail to insert an element only by throwing an exception.
     *
     * @throws NullPointerException if the specified element is null
     */
    public boolean offer(E e) {
        checkNotNull(e);//判斷是否為空,如果元素為null,則丟擲異常
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            if (count == items.length)
                return false;
            else {
                enqueue(e);
                return true;
            }
        } finally {
            lock.unlock();
        }
    }
 /**
     * 將元素插入陣列尾部,並喚醒等待取元素的執行緒。
     */
    private void enqueue(E x) {
        // assert lock.getHoldCount() == 1;
        // assert items[putIndex] == null;
        final Object[] items = this.items;
        items[putIndex] = x;
        if (++putIndex == items.length)
            putIndex = 0;
        count++;
        notEmpty.signal();
    }
/**
     *向陣列中插入元素,如果沒有空間就等待。
     *
     * @throws InterruptedException {@inheritDoc}
     * @throws NullPointerException {@inheritDoc}
     */
    public void put(E e) throws InterruptedException {
        checkNotNull(e);
        final ReentrantLock lock = this.lock;
        lock.lockInterruptibly();
        try {
            while (count == items.length)
                notFull.await();
            enqueue(e);
        } finally {
            lock.unlock();
        }
    }

ArrayBlockingQueue出隊操作

    出隊操作也是要加鎖的,以remove為例,從頭開始遍歷,一直到尾部標記的地方為止,當找到一個和所給元素相等的元素時,刪除這個節點的元素。至於take方法和poll方法都是刪除頭部元素,區別在於take方法會等待,而poll方法如果沒有元素可取則會直接返回null。

 /**
     * Removes a single instance of the specified element from this queue,
     * if it is present.  More formally, removes an element {@code e} such
     * that {@code o.equals(e)}, if this queue contains one or more such
     * elements.
     * Returns {@code true} if this queue contained the specified element
     * (or equivalently, if this queue changed as a result of the call).
     *
     * <p>Removal of interior elements in circular array based queues
     * is an intrinsically slow and disruptive operation, so should
     * be undertaken only in exceptional circumstances, ideally
     * only when the queue is known not to be accessible by other
     * threads.
     *
     * @param o element to be removed from this queue, if present
     * @return {@code true} if this queue changed as a result of the call
     */
    public boolean remove(Object o) {
        if (o == null) return false;
        final Object[] items = this.items;
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            if (count > 0) {
                final int putIndex = this.putIndex;
                int i = takeIndex;
                do {
                    if (o.equals(items[i])) {
                        removeAt(i);
                        return true;
                    }
                    if (++i == items.length)
                        i = 0;
                } while (i != putIndex);
            }
            return false;
        } finally {
            lock.unlock();
        }
    }
 public E take() throws InterruptedException {
        final ReentrantLock lock = this.lock;
        lock.lockInterruptibly();
        try {
            while (count == 0)//當元素為0時,會自動阻塞到條件佇列中去。知道被其他方法喚醒。
                notEmpty.await();
            return dequeue();
        } finally {
            lock.unlock();
        }
    }

 /**
     * Extracts element at current take position, advances, and signals.
     * Call only when holding lock.
     */
    private E dequeue() {
        // assert lock.getHoldCount() == 1;
        // assert items[takeIndex] != null;
        final Object[] items = this.items;
        @SuppressWarnings("unchecked")
        E x = (E) items[takeIndex];
        items[takeIndex] = null;
        if (++takeIndex == items.length)
            takeIndex = 0;
        count--;
        if (itrs != null)
            itrs.elementDequeued();
        notFull.signal();
        return x;
    }

ArrayBlockingQueue總結

       ArrayBlockingQueue本質上就是對一個陣列進行操作,它嚴格不允許元素為null,但是這些操作都是加了鎖的操作,因此它們都是執行緒安全的,並且可以根據實際情況來選擇鎖的公平非公平,公平鎖可以嚴格保證執行緒不會飢餓,而非公平鎖則可以提高系統的吞吐量。並且由於它還是一個佇列,對於佇列的操作也大多數都是在頭部或者是尾部的操作。除了鎖之外,ArrayBlockingQueue還提供了兩個condition來實現等待操作,這裡的方法其實就是把那些被阻塞的執行緒放在Condition佇列中,然後當有signal操作是就喚醒最前面的執行緒執行。整體而言這些操作就是在陣列的基礎之上加鎖,相對簡單。




LinkedBlockingQueue原始碼分析

     類似與ArrayList和LinkedList,和ArrayBlockingQueue對應的是LinkedBlockingqueue,不同的地方在於一個底層是陣列實現,一個底層是當連結串列來實現。再就是一個底層只有一把鎖,而另一個有兩把鎖。首先來看一下其中的重要引數。

   LinkedBlockingQueue有兩把鎖,對應著隊尾和隊頭的操作,也就是說新增和刪除操作不互斥,這和上面的是不一樣的,因此其併發量要高於ArrayBlockingQueue。

 /**
     * 這裡的節點相對簡單,單向連結串列。
     */
    static class Node<E> {
        E item;

        /**
         * One of:
         * - the real successor Node
         * - this Node, meaning the successor is head.next
         * - null, meaning there is no successor (this is the last node)
         */
        Node<E> next;

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

    /** 記錄連結串列容量 */
    private final int capacity;

    /**元素數目 */
    private final AtomicInteger count = new AtomicInteger();

    /**
     * 頭結點
     */
    transient Node<E> head;

    /**
     * 尾節點
     */
    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入隊操作

      以offer方法為例子,當我們嘗試入隊一個null時就會丟擲異常。其他情況下當容量不夠時就返回false,否則就可以給這個入隊操作進行加鎖,當元素個數小於容量時就新增節點到尾部然後給count+1,喚醒其他被阻塞的新增元素執行緒。這裡獲取的count是還沒有加一之前的值,因此它的值如果為0,那麼至少佇列中還是有一個元素的,可以喚醒消費執行緒消費了。

  /**
     * Inserts the specified element at the tail of this queue if it is
     * possible to do so immediately without exceeding the queue's capacity,
     * returning {@code true} upon success and {@code false} if this queue
     * is full.
     * When using a capacity-restricted queue, this method is generally
     * preferable to method {@link BlockingQueue#add add}, which can fail to
     * insert an element only by throwing an exception.
     *
     * @throws NullPointerException if the specified element is null
     */
    public boolean offer(E e) {
        if (e == null) throw new NullPointerException();//為null丟擲異常
        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;
        putLock.lock();//獲取put鎖
        try {
            if (count.get() < capacity) {//count小於容量在隊尾入隊
                enqueue(node);
                c = count.getAndIncrement();//count加一
                if (c + 1 < capacity)
                    notFull.signal();//仍然有剩餘容量,喚醒等待的put執行緒
            }
        } finally {
            putLock.unlock();
        }
        if (c == 0)
            signalNotEmpty();//喚醒消費執行緒
        return c >= 0;
    }
 /**
     * 這個方法意味這last.next=node,也就是把node作為last的下一個節點。
     *然後將last=last.next,也就是把last向後移。
     * @param node the node
     */
    private void enqueue(Node<E> node) {
        // assert putLock.isHeldByCurrentThread();
        // assert last.next == null;
        last = last.next = node;
    }
 /**
     * Signals a waiting take. Called only from put/offer (which do not
     * otherwise ordinarily lock takeLock.)
     */
    private void signalNotEmpty() {
        final ReentrantLock takeLock = this.takeLock;
        takeLock.lock();
        try {
            notEmpty.signal();
        } finally {
            takeLock.unlock();
        }
    }

LinkedBlockingQueue出隊操作

     先來看一下remove方法的出隊操作。由於remove方法要刪除的元素不固定,可能出現在佇列中的任何地方,因此需要同時鎖住隊首和隊尾兩把鎖,然後從頭到尾諸葛遍歷元素,當找到元素之後就刪除這個元素。這裡其實類似於單連結串列中的節點刪除,不同的地方在於要加鎖!

 /**
     * Removes a single instance of the specified element from this queue,
     * if it is present.  More formally, removes an element {@code e} such
     * that {@code o.equals(e)}, if this queue contains one or more such
     * elements.
     * Returns {@code true} if this queue contained the specified element
     * (or equivalently, if this queue changed as a result of the call).
     *
     * @param o element to be removed from this queue, if present
     * @return {@code true} if this queue changed as a result of the call
     */
    public boolean remove(Object o) {
        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();
        }
    }
   poll 方法返回隊頭元素並刪除之。如果佇列中沒有資料就返回null,否則的話就說明有元素,那麼就可以講一個元素出列,同時將count值減一,C>1意味著佇列中至少有一個元素,因此可以喚醒等待著的消費執行緒進行消費。當c的值等於容量時,此時c的實際值是容量減一,可以喚醒等待新增元素的執行緒進行新增。因為他們之前最有可能會被阻塞。
 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();//獲得count值並將其減一
                if (c > 1)
                    notEmpty.signal();//至少有一個元素,喚醒等待著的消費執行緒。
            }
        } finally {
            takeLock.unlock();
        }
        if (c == capacity)
            signalNotFull();喚醒
        return x;
    }

LinkedBlockingQueue總結

     LinkedBlockingQueue本質上是一個連結串列,區別於普通連結串列的地方在於它在隊首和隊尾的地方加了兩把鎖,分別對應於生產元素和消費元素,因此和獨佔鎖比起來會快一些(畢竟可以首尾同時操作),它本身的元素也是不允許為null的。


ArrayBlockingQueue和LinkedBlockingQueue比較

1.ABQ的底層是陣列實現,LBQ的底層是連結串列實現。

2.ABQ加鎖只加一把,並且是全域性的,而LBQ的鎖有兩把,分別對應著隊首和隊尾,同時也有兩個Condition佇列。也就是說,ABQ的取元素和放元素是互斥的,而LBQ則沒有相互關聯,因此就併發性而言,LBQ要優於ABQ。

3.ABQ 的容量是必須需要的,並且不可以擴容,而LBQ的大小可以指定,也可以不指定,不指定的情況下其值為最大整數值。

4.ABQ 支援公平鎖和非公平鎖,而LBQ不可指定,其本身內部使用的也是非公平鎖。




參考資料:

http://cmsblogs.com/?p=2381

https://blog.csdn.net/u014082714/article/details/52215130

https://blog.csdn.net/u010412719/article/details/52337471

https://blog.csdn.net/javazejian/article/details/77410889?locationNum=1&fps=1#arrayblockingqueue的阻塞新增的實現原理

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


相關文章