Java併發容器之阻塞隊列LinkedBlockingQueue

在前一篇文章《Java併發容器之有界阻塞隊列ArrayBlockingQueue》中講到了阻塞隊列,提到了ArrayBlockingQueue,在這篇文章中我們來看另一種線程安全的阻塞隊列實現-LinkedBlockingQueue。

ArrayBlockingQueue底層使用數組來保存隊列元素,但是LinkedBlockingQueue底層使用列表來保存隊列元素。

下面我們先對LinkedBlockingQueue實現進行一個簡單的說明,之後結合前一篇文章對ArrayBlockingQueue的描述,總結一下兩者的區別。

LinkedBlockingQueue的實現原理

LinkedBlockingQueue類提供了三種構造方法用於實例化對象,分別如下所示:

/**
     * Creates a {@code LinkedBlockingQueue} with a capacity of
     * {@link Integer#MAX_VALUE}.
     */
    public LinkedBlockingQueue() {
        this(Integer.MAX_VALUE);
    }

    /**
     * Creates a {@code LinkedBlockingQueue} with the given (fixed) capacity.
     *
     * @param capacity the capacity of this queue
     * @throws IllegalArgumentException if {@code capacity} is not greater
     *         than zero
     */
    public LinkedBlockingQueue(int capacity) {
        if (capacity <= 0) throw new IllegalArgumentException();
        this.capacity = capacity;
        last = head = new Node<E>(null);
    }

    /**
     * Creates a {@code LinkedBlockingQueue} with a capacity of
     * {@link Integer#MAX_VALUE}, initially containing the elements of the
     * given collection,
     * added in traversal order of the collection's iterator.
     *
     * @param c the collection of elements to initially contain
     * @throws NullPointerException if the specified collection or any
     *         of its elements are null
     */
    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) {
                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來初始化隊列的容量。一般情況下我們最好設置隊列的容量,否則很容易造成內存溢出問題。在第三個構造函數中,使用已有的集合對象初始化隊列,內部通過先構建一個空隊列然後遍歷集合中的每個元素並將每個元素添加到隊列中。

隊列中的每個元素節點都是一個Node,定義如下,包含元素對象e以及下一個節點Node的引用,

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

在LinkedBlockingQueue內部維護了兩個Node引用,分別指向這個鏈表中的隊列頭元素和隊列尾元素。

 /**
     * Head of linked list.
     * Invariant: head.item == null
     */
    private transient Node<E> head;

    /**
     * Tail of linked list.
     * Invariant: last.next == null
     */
    private transient Node<E> last;

下面我們重點來看下是怎麼添加和刪除元素?

添加元素

在前一篇文章我們知道,對於阻塞隊列來說,添加元素的方式其實有三種,分別是add、offer和put,下面分別來看一下。

(1)add

public boolean add(E e) {
        if (offer(e))
            return true;
        else
            throw new IllegalStateException("Queue full");
    }

add底層複用的是offer方法,在offer方法成功後返回成功,如果offer方法調用失敗,則拋出異常。

(2)offer

public boolean offer(E e) {
        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);
        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;
    }

這裏的Offer()方法做了兩件事,第一件事是判斷隊列是否滿,滿了就直接釋放鎖,沒滿就將節點封裝成Node入隊,然後再次判斷隊列添加完成後是否已滿,不滿就繼續喚醒等待在條件對象notFull上的添加線程。第二件事是,判斷是否需要喚醒等待在notEmpty條件對象上的消費線程。這裏我們可能會有點疑惑,爲什麼添加完成後是繼續喚醒在條件對象notFull上的添加線程而不是像ArrayBlockingQueue那樣直接喚醒notEmpty條件對象上的消費線程?而又爲什麼要當if (c == 0)時纔去喚醒消費線程呢?

  • 喚醒添加線程的原因,在添加新元素完成後,會判斷隊列是否已滿,不滿就繼續喚醒在條件對象notFull上的添加線程,這點與前面分析的ArrayBlockingQueue很不相同,在ArrayBlockingQueue內部完成添加操作後,會直接喚醒消費線程對元素進行獲取,這是因爲ArrayBlockingQueue只用了一個ReenterLock同時對添加線程和消費線程進行控制,這樣如果在添加完成後再次喚醒添加線程的話,消費線程可能永遠無法執行,而對於LinkedBlockingQueue來說就不一樣了,其內部對添加線程和消費線程分別使用了各自的ReenterLock鎖對併發進行控制,也就是說添加線程和消費線程是不會互斥的,所以添加鎖只要管好自己的添加線程即可,添加線程自己直接喚醒自己的其他添加線程,如果沒有等待的添加線程,直接結束了。如果有就直到隊列元素已滿才結束掛起,當然offer方法並不會掛起,而是直接結束,只有put方法纔會當隊列滿時才執行掛起操作。注意消費線程的執行過程也是如此。這也是爲什麼LinkedBlockingQueue的吞吐量要相對大些的原因。

  • 爲什麼要判斷if (c == 0)時纔去喚醒消費線程呢,這是因爲消費線程一旦被喚醒是一直在消費的(前提是有數據),所以c值是一直在變化的,c值是添加完元素前隊列的大小,此時c只可能是0或c>0,如果是c=0,那麼說明之前消費線程已停止,條件對象上可能存在等待的消費線程,添加完數據後應該是c+1,那麼有數據就直接喚醒等待消費線程,如果沒有就結束啦,等待下一次的消費操作。如果c>0那麼消費線程就不會被喚醒,只能等待下一個消費操作(poll、take、remove)的調用,那爲什麼不是條件c>0纔去喚醒呢?我們要明白的是消費線程一旦被喚醒會和添加線程一樣,一直不斷喚醒其他消費線程,如果添加前c>0,那麼很可能上一次調用的消費線程後,數據並沒有被消費完,條件隊列上也就不存在等待的消費線程了,所以c>0喚醒消費線程得意義不是很大,當然如果添加線程一直添加元素,那麼一直c>0,消費線程執行的換就要等待下一次調用消費操作了(poll、take、remove)。

移除元素

移除元素的方法有remove、poll、take。下面來一一分析。

(1)remove方法,源碼如下所示



這裏的操作是在加鎖的控制下進行的,並且同時加了putLock和takeLock,主要還是基於線程安全性考慮。在內部邏輯上,通過for循環遍歷整個鏈表,找到對應的節點後將相應節點從鏈表中移除並返回成功。

(2)poll方法,源碼如下:


在poll方法中首先判斷鏈表是否爲空,如果爲空,直接返回null,否則進行加鎖控制,並在鎖的保護下從隊列的頭部取出第一個元素返回。如果在取出一個元素之後發現隊列中還有元素,通過notEmpty.signal()來通知其他等待在這個隊列上的消費線程。

(3)take方法

源碼如下:

take方法是一個可阻塞可中斷的移除方法,主要做了兩件事,一是,如果隊列沒有數據就掛起當前線程到 notEmpty條件對象的等待隊列中一直等待,如果有數據就刪除節點並返回數據項,同時喚醒後續消費線程,二是嘗試喚醒條件對象notFull上等待隊列中的添加線程。

LinkedBlockingQueue和ArrayBlockingQueue的對比

1.隊列大小有所不同,ArrayBlockingQueue是有界的,初始化必須指定大小,而LinkedBlockingQueue可以是有界的也可以是無界的(Integer.MAX_VALUE),對於後者而言,當添加速度大於移除速度時,在無界的情況下,可能會造成內存溢出等問題。

2.數據存儲容器不同,ArrayBlockingQueue採用的是數組作爲數據存儲容器,而LinkedBlockingQueue採用的則是以Node節點作爲連接對象的鏈表。

3.由於ArrayBlockingQueue採用的是數組的存儲容器,因此在插入或刪除元素時不會產生或銷燬任何額外的對象實例,而LinkedBlockingQueue則會生成一個額外的Node對象。這可能在長時間內需要高效併發地處理大批量數據的時,對於GC可能存在較大影響。

4.兩者的實現隊列添加或移除的鎖不一樣,ArrayBlockingQueue實現的隊列中的鎖是沒有分離的,即添加操作和移除操作採用的同一個ReenterLock鎖,而LinkedBlockingQueue實現的隊列中的鎖是分離的,其添加採用的是putLock,移除採用的則是takeLock,這樣能大大提高隊列的吞吐量,也意味着在高併發的情況下生產者和消費者可以並行地操作隊列中的數據,以此來提高整個隊列的併發性能。



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