一文弄懂Java線程安全隊列

一、分類

java中所有隊列都繼承至java.util.Queue接口,該接口定義了以下三組方法:

方法名 拋出異常 返回特殊值
插入 add(e) offer(e)
移除 remove() poll()
檢查 element() peek()

Java提供的線程安全的Queue可以分爲阻塞隊列和非阻塞隊列使用阻塞算法的隊列可以用一個鎖(入隊和出隊用同一把鎖)或兩個鎖(入隊和出隊用不同的鎖)等方式來實現,而非阻塞的實現方式則可以使用循環CAS的方式來實現, 其中阻塞隊列的典型例子是BlockingQueue,非阻塞隊列的典型例子是ConcurrentLinkedQueue。
在這裏插入圖片描述

二、BlockingQueue 阻塞隊列

BlockingQueue 對插入操作、移除操作、獲取元素操作提供了四種不同的方法用於不同的場景中使用:

  • 1、拋出異常;
  • 2、返回特殊值(null 或 true/false,取決於具體的操作);
  • 3、阻塞等待此操作,直到這個操作成功;
  • 4、阻塞等待此操作,直到成功或者超時指定時間。總結如下:
拋出異常 特殊值 阻塞 超時
插入 add(e) offer(e) put(e) offer(e, time, unit)
移除 remove() poll() take() poll(time, unit)
檢查 element() peek() 不可用 不可用

從上表可以很明顯看出每個方法的作用,這個不用多說。我想說的是:

  • add(e) remove() element()方法不會阻塞線程。當不滿足約束條件時,會拋出IllegalStateException 異常。例如:當隊列被元素填滿後,再調用add(e),則會拋出異常。
  • offer(e) poll() peek() 方法即不會阻塞線程,也不會拋出異常。例如:當隊列被元素填滿後,再調用offer(e),則不會插入元素,函數返回false。
  • 要想要實現阻塞功能,需要調用put(e) take()方法。 當不滿足約束條件時,會阻塞線程。其實質可以用一個鎖(入隊和出隊共享一把鎖)來實現線程安全。以ArrayBlockQueue源碼中put(e)/take()源碼如下:

    /**
     * Inserts the specified element at the tail of this queue, waiting
     * for space to become available if the queue is full.
     *
     * @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();
        }
    }

    /**
     * Inserts element at current put position, advances, and signals.
     * Call only when holding lock.
     */
    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();
    }



    public E take() throws InterruptedException {
        final ReentrantLock lock = this.lock;
        lock.lockInterruptibly();
        try {
            while (count == 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;
    }



BlockingQueue是個接口,有如下常用實現類:

  • ArrayBlockQueue:一個由數組支持的有界阻塞隊列。此隊列按 FIFO(先進先出)原則對元素進行排序。創建其對象必須明確大小,像數組一樣。
  • LinkedBlockQueue:一個可改變大小的阻塞隊列。此隊列按 FIFO(先進先出)原則對元素進行排序。創建其對象如果沒有明確大小,默認值是Integer.MAX_VALUE。鏈接隊列的吞吐量通常要高於基於數組的隊列,但是在大多數併發應用程序中,其可預知的性能要低。
  • PriorityBlockingQueue:類似於LinkedBlockingQueue,但其所含對象的排序不是FIFO,而是依據對象的自然排序順序或者是構造函數所帶的Comparator決定的順序。
  • SynchronousQueue:同步隊列。同步隊列沒有任何容量,每個插入必須等待另一個線程移除,反之亦然。

使用示例:

ArrayBlockQueue使用(生產者-消費者):https://www.jianshu.com/p/b1408e3e3bb4

三、ConcurrentLinkedQueue 非阻塞隊列

ConcurrentLinkedQueue是一個基於鏈接節點的無界線程安全隊列,它採用先進先出的規則對節點進行排序,當我們添加一個元素的時候,它會添加到隊列的尾部,當我們獲取一個元素時,它會返回隊列頭部的元素。基於CAS的“wait-free”(常規無等待)來實現,CAS並不是一個算法,它是一個CPU直接支持的硬件指令,這也就在一定程度上決定了它的平臺相關性。

再通過源碼來詳細分析下它是如何使用循環CAS的方式來入隊的(JDK1.8)


    public boolean offer(E e) {
        checkNotNull(e);
        //創建入隊節點
        final Node<E> newNode = new Node<E>(e);
        //t爲tail節點,p爲尾節點,默認相等,採用失敗即重試的方式,直到入隊成功
        for (Node<E> t = tail, p = t;;) {
            //獲得p的下一個節點
            Node<E> q = p.next;
            // 如果下一個節點是null,也就是p節點就是尾節點
            if (q == null) {
              //將入隊節點newNode設置爲當前隊列尾節點p的next節點
                if (p.casNext(null, newNode)) { 
                   //判斷tail節點是不是尾節點,也可以理解爲如果插入結點後tail節點和p節點距離達到兩個結點
                    if (p != t) 
                     //如果tail不是尾節點則將入隊節點設置爲tail。
                     // 如果失敗了,那麼說明有其他線程已經把tail移動過 
                        casTail(t, newNode);  
                    return true;
                }
            }
                 // 如果p節點等於p的next節點,則說明p節點和q節點都爲空,表示隊列剛初始化,所以返回 head節點
            else if (p == q)
                p = (t != (t = tail)) ? t : head;
            else
                //p有next節點,表示p的next節點是尾節點,則需要重新更新p後將它指向next節點
                p = (p != t && t != (t = tail)) ? t : q;
        }
    }



    public E poll() {
        // 設置起始點  
        restartFromHead:
        for (;;) {
        //p表示head結點,需要出隊的節點
            for (Node<E> h = head, p = h, q;;) {
            //獲取p節點的元素
                E item = p.item;
                //如果p節點的元素不爲空,使用CAS設置p節點引用的元素爲null
                if (item != null && p.casItem(item, null)) {

                    if (p != h) // hop two nodes at a time
                    //如果p節點不是head節點則更新head節點,也可以理解爲刪除該結點後檢查head是否與頭結點相差兩個結點,如果是則更新head節點
                        updateHead(h, ((q = p.next) != null) ? q : p);
                    return item;
                }
                //如果p節點的下一個節點爲null,則說明這個隊列爲空,更新head結點
                else if ((q = p.next) == null) {
                    updateHead(h, p);
                    return null;
                }
                //結點出隊失敗,重新跳到restartFromHead來進行出隊
                else if (p == q)
                    continue restartFromHead;
                else
                    p = q;
            }
        }
    }

ConcurrentLinkedQueue 的非阻塞算法實現主要可概括爲下面幾點:

  • 使用 CAS 原子指令來處理對數據的併發訪問,這是非阻塞算法得以實現的基礎。
  • head/tail 並非總是指向隊列的頭 / 尾節點,也就是說允許隊列處於不一致狀態。 這個特性把入隊/出隊時,原本需要一起原子化執行的兩個步驟分離開來,從而縮小了入隊 /出隊時需要原子化更新值的範圍到唯一變量。這是非阻塞算法得以實現的關鍵。
  • 以批處理方式來更新head/tail,從整體上減少入隊 / 出隊操作的開銷。

使用示例:

Java ConcurrentLinkedQueue隊列線程安全操作:https://yq.aliyun.com/articles/615890/

參考:

解讀 Java 併發隊列 BlockingQueue:https://javadoop.com/post/java-concurrent-queue

Java多線程高併發學習筆記——阻塞隊列:https://cloud.tencent.com/developer/article/1090012

Java線程安全隊列:https://www.jianshu.com/p/ad6ef76e067a

第二十一章、java線程安全隊列:https://www.jianshu.com/p/04aeb0088dec

java併發之ConcurrentLinkedQueue:https://www.jianshu.com/p/24516e7853d1

ConcurrentLinkedQueue的實現原理和源碼分析:https://www.jianshu.com/p/26d9745614dd

Java線程(十):CAS:https://www.kancloud.cn/digest/java-thread/107465

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