Java併發包concurrent——BlockingQueue

目錄

1. BlockingQueue接口

2. BlockingQueue的分類

3. 有界阻塞隊列——ArrayBlockingQueue和LinkedBlockingQueue

4. 優先無界阻塞隊列——PriorityBlockingQueue

5. 同步阻塞隊列——SynchronousQueue

6. 延時阻塞隊列——DelayQueue


BlockingQueue是java.util.concurrent包中定義的一個阻塞隊列的接口,concurrent包中實現了多種阻塞隊列,它們都實現了該接口。有了這些阻塞隊列,可以更好的實現多線程之間的數據共享;而如果能靈活運用這些類,便可以在工作中更好的實現高質量的多線程高併發程序。本章首先從BlockingQueue接口開始,詳細分析了concurrent包中的這些阻塞隊列的實現。

1. BlockingQueue接口

        BlockingQueue接口繼承了Queue接口,所以其本身也可以作爲一般的隊列使用。在此基礎上,BlockingQueue定義瞭如下幾個阻塞方法:

  1. void put(E e) throws InterruptedException; 該方法爲阻塞式入隊方法,如果發生中斷異常會拋出。
  2. boolean offer(E e, long timeout, TimeUnit unit) throws InterruptedException; 該方法爲阻塞式入隊方法,該方法可以設置一個超時時間,當發生超時時可返回false表示入隊失敗。
  3. E take() throws InterruptedException; 該方法爲阻塞式出隊方法,如果發生中斷異常會拋出。
  4. E poll(long timeout, TimeUnit unit) throws InterruptedException; 該方法爲阻塞式出對方法,該方法也可以設置一個超時時間,如果發生超時,將會返回null。

2. BlockingQueue的分類

        concurrent包中定義了多種阻塞隊列:

  1.  ArrayBlockingQueue:有界阻塞隊列,通過數組實現,在創建對象時必須指定該隊列的容量。
  2.  LinkedBlockingQueue:有界阻塞隊列,通過鏈表實現,在創建對象時可以指定該隊列的容量,如果不指定容量默認爲Integer類型的最大值。
  3.  PriorityBlockingQueue:無界阻塞隊列,可指定初始容量,可指定隊列的優先級。
  4. SynchronousQueue:同步阻塞隊列,該阻塞隊列比較特殊,其不存儲元素,如果該隊列中有一個元素,則其他線程不能再往裏面插入元素;只有等消費者線程消費之後才能繼續插入元素,反之也是一樣。
  5. DelayQueue:延時阻塞隊列,該隊列可設置延時,入隊的元素只有在延時期滿之後才能消費。
  6. LinkedTransferQueue:無界阻塞隊列,通過鏈表實現,相比其他隊列,該隊列多了兩個方法transfer()和tryTransfer()。
  7. LinkedBlockingDeque:雙向阻塞隊列,該隊列也是通過鏈表實現,多線程併發時可將鎖競爭縮減一半。

        本節將concurrent包中所有的阻塞隊列的特點進行了簡要的概括,下文將詳細分析每種阻塞隊列的實現原理與使用場景等。

3. 有界阻塞隊列——ArrayBlockingQueue和LinkedBlockingQueue

        concurrent包實現了兩種有界阻塞隊列:ArrayBlockingQueue和LinkedBlockingQueue,顧名思義,ArrayBlockingQueue通過數組實現阻塞隊列,LinkedBlockingQueue則通過鏈表實現阻塞隊列。兩者的實現方案類似,因此,在此以ArrayBlockingQueue爲例,介紹有界阻塞隊列的實現原理。

        在ArrayBlockingQueue中定義了兩個方法用來爲阻塞隊列插入和刪除元素。

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

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

        其中,enqueue方法爲隊列添加元素,dequeue方法爲隊列刪除元素。從這兩個方法可以看出,ArrayBlockingQueue中定義了一個items數組用來保存阻塞隊列的元素,然後分別定義了putIndex和takeIndex來表示插入和刪除元素的位置。當putIndex或takeIndex增長爲數組的長度時,會將其置零,表示該隊列爲一個循環隊列。ArrayBlockingQueue中的元素個數則用count來統計。ArrayBlockingQueue可通過構造方法指定阻塞隊列的容量capacity。

        ArrayBlockingQueue基於ReentrantLock和Condition來實現阻塞隊列和線程安全。ArrayBlockingQueue定義了一個ReentrantLock對象lock和兩個Condition對象notEmpty和notFull。首先,在調用阻塞隊列相關的方法時,通過ReentrantLock爲操作加鎖;然後進行入隊和出隊操作。入隊時,如果隊列已滿,對於阻塞類的入隊方法會調用notFull的await等待,直到有線程調用出隊方法,此時會觸發notFull的signal方法通知阻塞的線程入隊。反之,如果隊列爲空,對於阻塞類的出隊方法會調用notEmpty的await方法等待,直到有線程調用入隊方法,就會觸發notEmpty的signal方法通知阻塞的線程出隊。而對於非阻塞類的入隊和出對方法,入隊和出隊則直接返回,不會阻塞。

        LinkedBlockingQueue是基於鏈表的有界阻塞隊列,因爲都是有界阻塞隊列,所以其實現與ArrayBlockingQueue類似,在此不再贅述其實現原理。這裏僅總結一下ArrayBlockingQueue和LinkedBlockingQueue的異同點,以便在平時應用中選擇更加合適的隊列。

        ArrayBlockingQueue和LinkedBlockingQueue都是有界阻塞隊列,也就是說隊列的容量有限制,都是通過構造函數來指定隊列的容量。而其不同點主要體現在以下幾個方面:

  • ArrayBlockingQueue是基於數組實現的,因此隊列的每個元素即表示爲數組的一個元素;而LinkedBlockingQueue是基於鏈表實現的,隊列的每個元素需要額外的一個Node對象來保存。
  • ArrayBlockingQueue出隊和入隊使用同一個ReentrantLock對象加鎖;而LinkedBlockingQueue的出隊和入隊使用不同的兩個ReentrantLock對象加鎖。
  • ArrayBlockingQueue可以在構造函數中指定鎖爲公平鎖,默認爲非公平鎖。
  • 對於LinkedBlockingQueue,還有一點需要注意,如果創建LinkedBlockingQueue對象時不爲其指定容量,則其容量默認爲Integer.MAX_VALUE,所以如果使用不當,很可能將系統內存耗盡,使用時需要特別注意,推薦爲其指定固定的容量。

4. 優先無界阻塞隊列——PriorityBlockingQueue

        PriorityBlockingQueue是一個可以指定優先級的無界阻塞隊列,其實現原理是在該隊列中添加了一個Comparator比較器用於排序。

        首先,PriorityBlockingQueue是一個無界阻塞隊列,因此其在創建時指定的容量只是一個初始容量,隨着元素的不斷增多,其會調用tryGrow方法進行擴容。

    private void tryGrow(Object[] array, int oldCap) {
        lock.unlock(); // must release and then re-acquire main lock
        Object[] newArray = null;
        if (allocationSpinLock == 0 &&
            UNSAFE.compareAndSwapInt(this, allocationSpinLockOffset,
                                     0, 1)) {
            try {
                int newCap = oldCap + ((oldCap < 64) ?
                                       (oldCap + 2) : // grow faster if small
                                       (oldCap >> 1));
                if (newCap - MAX_ARRAY_SIZE > 0) {    // possible overflow
                    int minCap = oldCap + 1;
                    if (minCap < 0 || minCap > MAX_ARRAY_SIZE)
                        throw new OutOfMemoryError();
                    newCap = MAX_ARRAY_SIZE;
                }
                if (newCap > oldCap && queue == array)
                    newArray = new Object[newCap];
            } finally {
                allocationSpinLock = 0;
            }
        }
        if (newArray == null) // back off if another thread is allocating
            Thread.yield();
        lock.lock();
        if (newArray != null && queue == array) {
            queue = newArray;
            System.arraycopy(array, 0, newArray, 0, oldCap);
        }
    }

        擴容時,首先會釋放鎖,然後計算新數組的容量,然後再爲主線程加鎖的方式進行。其中,容量過大可能會溢出,拋出OOM異常。

        介紹了擴容之後,接着對隊列的入隊和出隊方法進行介紹。PriorityBlockingQueue使用堆排序算法對元素進行排序。

    public boolean offer(E e) {
        if (e == null)
            throw new NullPointerException();
        final ReentrantLock lock = this.lock;
        lock.lock();
        int n, cap;
        Object[] array;
        while ((n = size) >= (cap = (array = queue).length))
            tryGrow(array, cap);
        try {
            Comparator<? super E> cmp = comparator;
            if (cmp == null)
                siftUpComparable(n, e, array);
            else
                siftUpUsingComparator(n, e, array, cmp);
            size = n + 1;
            notEmpty.signal();
        } finally {
            lock.unlock();
        }
        return true;
    }

      入隊時,如果元素的數量超過了當前的容量,則首先進行擴容操作;然後,會根據比較器進行插入操作。其會調用隊列中的siftUpComparable或siftUpUsingComparator方法插入數據,具體根據創建PriorityBlockingQueue對象時是否傳入比較器決定。

    private static <T> void siftUpComparable(int k, T x, Object[] array) {
        Comparable<? super T> key = (Comparable<? super T>) x;
        while (k > 0) {
            int parent = (k - 1) >>> 1;
            Object e = array[parent];
            if (key.compareTo((T) e) >= 0)
                break;
            array[k] = e;
            k = parent;
        }
        array[k] = key;
    }

    private static <T> void siftUpUsingComparator(int k, T x, Object[] array,
                                       Comparator<? super T> cmp) {
        while (k > 0) {
            int parent = (k - 1) >>> 1;
            Object e = array[parent];
            if (cmp.compare(x, (T) e) >= 0)
                break;
            array[k] = e;
            k = parent;
        }
        array[k] = x;
    }

        由代碼可知,PriorityBlockingQueue採用堆排序保持插入元素的順序,並以此來保證優先級。因爲其是一個無界隊列,因此元素入隊時是不會產生阻塞的。

        元素出隊則根據排序的元素順序進行出隊,主要調用了dequeue方法出隊。

    private E dequeue() {
        int n = size - 1;
        if (n < 0)
            return null;
        else {
            Object[] array = queue;
            E result = (E) array[0];
            E x = (E) array[n];
            array[n] = null;
            Comparator<? super E> cmp = comparator;
            if (cmp == null)
                siftDownComparable(0, x, array, n);
            else
                siftDownUsingComparator(0, x, array, n, cmp);
            size = n;
            return result;
        }
    }

        該方法會取數組的第一個元素出隊,然後調用對應的siftDownComparable或siftDownUsingComparator方法保持堆的數據結構。

    private static <T> void siftDownComparable(int k, T x, Object[] array,
                                               int n) {
        if (n > 0) {
            Comparable<? super T> key = (Comparable<? super T>)x;
            int half = n >>> 1;           // loop while a non-leaf
            while (k < half) {
                int child = (k << 1) + 1; // assume left child is least
                Object c = array[child];
                int right = child + 1;
                if (right < n &&
                    ((Comparable<? super T>) c).compareTo((T) array[right]) > 0)
                    c = array[child = right];
                if (key.compareTo((T) c) <= 0)
                    break;
                array[k] = c;
                k = child;
            }
            array[k] = key;
        }
    }

    private static <T> void siftDownUsingComparator(int k, T x, Object[] array,
                                                    int n,
                                                    Comparator<? super T> cmp) {
        if (n > 0) {
            int half = n >>> 1;
            while (k < half) {
                int child = (k << 1) + 1;
                Object c = array[child];
                int right = child + 1;
                if (right < n && cmp.compare((T) c, (T) array[right]) > 0)
                    c = array[child = right];
                if (cmp.compare(x, (T) c) <= 0)
                    break;
                array[k] = c;
                k = child;
            }
            array[k] = x;
        }
    }

5. 同步阻塞隊列——SynchronousQueue

        SynchronousQueue是一個同步阻塞隊列,其本身並不保存元素。每個入隊操作都必須等待一個出隊操作才能繼續下一個入隊操作。

        SynchronousQueue隊列中實現了兩個Transferer接口:TransferStack和TransferQueue,分別用來實現非公平策略和公平策略,這兩種類型的底層則都是通過鏈表來實現的。Transferer接口是一個爲雙堆棧和隊列提供的共享內部API,及在SynchronousQueue中,入隊和出對方法都會調用Transferer接口的transfer方法。

    /**
     * Shared internal API for dual stacks and queues.
     */
    abstract static class Transferer<E> {
        /**
         * Performs a put or take.
         *
         * @param e if non-null, the item to be handed to a consumer;
         *          if null, requests that transfer return an item
         *          offered by producer.
         * @param timed if this operation should timeout
         * @param nanos the timeout, in nanoseconds
         * @return if non-null, the item provided or received; if null,
         *         the operation failed due to timeout or interrupt --
         *         the caller can distinguish which of these occurred
         *         by checking Thread.interrupted.
         */
        abstract E transfer(E e, boolean timed, long nanos);
    }

        下面以TransferStack爲例介紹SynchronousQueue隊列的實現原理。

        /**
         * Puts or takes an item.
         */
        @SuppressWarnings("unchecked")
        E transfer(E e, boolean timed, long nanos) {
            SNode s = null; // constructed/reused as needed
            int mode = (e == null) ? REQUEST : DATA;

            for (;;) {
                SNode h = head;
                if (h == null || h.mode == mode) {  // empty or same-mode
                    if (timed && nanos <= 0) {      // can't wait
                        if (h != null && h.isCancelled())
                            casHead(h, h.next);     // pop cancelled node
                        else
                            return null;
                    } else if (casHead(h, s = snode(s, e, h, mode))) {
                        SNode m = awaitFulfill(s, timed, nanos);
                        if (m == s) {               // wait was cancelled
                            clean(s);
                            return null;
                        }
                        if ((h = head) != null && h.next == s)
                            casHead(h, s.next);     // help s's fulfiller
                        return (E) ((mode == REQUEST) ? m.item : s.item);
                    }
                } else if (!isFulfilling(h.mode)) { // try to fulfill
                    if (h.isCancelled())            // already cancelled
                        casHead(h, h.next);         // pop and retry
                    else if (casHead(h, s=snode(s, e, h, FULFILLING|mode))) {
                        for (;;) { // loop until matched or waiters disappear
                            SNode m = s.next;       // m is s's match
                            if (m == null) {        // all waiters are gone
                                casHead(s, null);   // pop fulfill node
                                s = null;           // use new node next time
                                break;              // restart main loop
                            }
                            SNode mn = m.next;
                            if (m.tryMatch(s)) {
                                casHead(s, mn);     // pop both s and m
                                return (E) ((mode == REQUEST) ? m.item : s.item);
                            } else                  // lost match
                                s.casNext(m, mn);   // help unlink
                        }
                    }
                } else {                            // help a fulfiller
                    SNode m = h.next;               // m is h's match
                    if (m == null)                  // waiter is gone
                        casHead(h, null);           // pop fulfilling node
                    else {
                        SNode mn = m.next;
                        if (m.tryMatch(h))          // help match
                            casHead(h, mn);         // pop both h and m
                        else                        // lost match
                            h.casNext(m, mn);       // help unlink
                    }
                }
            }
        }

        TransferStack和TransferQueue都是Transfer接口的實現類,這兩個類都通過實現transfer()方法達到同步隊列的目的。對於TransferStack類,棧中的每個節點包含三種模式:

  • REQUEST:表示節點是一個未完成消費的消費者;
  • DATA:表示節點是一個未完成生產的生產者;
  • FULFILL:表示節點正在執行另一個未完成的生產者或消費者。

        transfer根據傳入的參數判斷當前調用方法是什麼模式。根據這三種模式,transfer方法的處理方式也分爲以下三種:

  • 棧頂元素如果明顯爲空或已經包含相同模式的節點,則嘗試push節點到棧中並等待匹配,返回它,如果取消則返回null。
  • 如果明顯包含互補模式的節點,則嘗試push節點到棧中並與對應的等待節點匹配,並從堆棧中pop出這兩個節點,返回匹配的項。由於其他線程可能在執行第三種情況,匹配或取消鏈接可能沒有必要。
  • 如果棧頂已經擁有另一個已完成的節點那麼通過執行匹配或pop操作來幫助它出棧,並繼續操作。幫助的代碼本質上與實際代碼相同,只是其不返回值。

        TransferQueue與TransferStack實現類似,只是底層的數據結構爲隊列。在此不再進行分析,如果感興趣可以自行閱讀源碼。

        介紹了Transfer接口與實現,接下來便是 SynchronousQueue的各個方法如何通過Transfer接口實現同步隊列。其實SynchronousQueue的方法實現非常簡單,不管是入隊還是出隊,都是調用Transfer接口的transfer方法來完成的。對於入隊方法,transfer的第一個參數不爲空;而對於出隊方法,transfer的第一個參數爲空。

6. 延時阻塞隊列——DelayQueue

        首先,要使用延時隊列,存放的元素必須實現Delayed接口。該接口的定義如下:

public interface Delayed extends Comparable<Delayed> {

    /**
     * Returns the remaining delay associated with this object, in the
     * given time unit.
     *
     * @param unit the time unit
     * @return the remaining delay; zero or negative values indicate
     * that the delay has already elapsed
     */
    long getDelay(TimeUnit unit);
}

        該接口只有一個方法getDeplay,該方法返回給定單位的與該對象關聯的剩餘延遲時間。

        DeplayQueue隊列通過PriorityQueue來實現延時隊列,即DeplayQueue會根據元素的延遲時間來排序。

    public boolean offer(E e) {
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            q.offer(e);
            if (q.peek() == e) {
                leader = null;
                available.signal();
            }
            return true;
        } finally {
            lock.unlock();
        }
    }

        入隊時,通過調用PriorityQueue的offer()方法入隊,保持隊列元素的排列順序;然後調用peek()方法獲取優先隊列的隊首元素,該元素爲延遲時間最小的元素,即即將過期,此時會調用available的signal()方法通知等待的線程有元素可用;然後返回結果。實現較爲簡單。

    public E poll(long timeout, TimeUnit unit) throws InterruptedException {
        long nanos = unit.toNanos(timeout);
        final ReentrantLock lock = this.lock;
        lock.lockInterruptibly();
        try {
            for (;;) {
                E first = q.peek();
                if (first == null) {
                    if (nanos <= 0)
                        return null;
                    else
                        nanos = available.awaitNanos(nanos);
                } else {
                    long delay = first.getDelay(NANOSECONDS);
                    if (delay <= 0)
                        return q.poll();
                    if (nanos <= 0)
                        return null;
                    first = null; // don't retain ref while waiting
                    if (nanos < delay || leader != null)
                        nanos = available.awaitNanos(nanos);
                    else {
                        Thread thisThread = Thread.currentThread();
                        leader = thisThread;
                        try {
                            long timeLeft = available.awaitNanos(delay);
                            nanos -= delay - timeLeft;
                        } finally {
                            if (leader == thisThread)
                                leader = null;
                        }
                    }
                }
            }
        } finally {
            if (leader == null && q.peek() != null)
                available.signal();
            lock.unlock();
        }
    }

        出隊時,不管是非阻塞式方法還是阻塞式方法,即實現都一樣。首先,通過peek()方法獲取隊首元素,如果該元素爲空或者該元素的getDelay()方法返回值大於0,即還未到期,則返回空或等待;反之,即該元素的getDelay()方法返回值小於0,則直接調用poll()方法返回該值。

        這裏,就介紹了大部分的阻塞隊列。而根據第二節中所介紹的,還有LinkedTransferQueue和LinkedBlockingDeque兩個隊列的實現原理沒有介紹。對於LinkedBlockingDeque,其與LinkedBlockingQueue類似,不過LinkedBlockingDeque是一個雙向隊列,所以在此不再多進行介紹。而LinkedTransferQueue,其實現原理比較複雜,這裏也不進行過多介紹,有機會可以在後期單獨對其實現原理進行分析。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章