計算機程式的思維邏輯 (76) - 併發容器 - 各種佇列

swiftma發表於2017-03-27

本系列文章經補充和完善,已修訂整理成書《Java程式設計的邏輯》(馬俊昌著),由機械工業出版社華章分社出版,於2018年1月上市熱銷,讀者好評如潮!各大網店和書店有售,歡迎購買:京東自營連結

計算機程式的思維邏輯 (76) - 併發容器 - 各種佇列

本節,我們來探討Java併發包中的各種佇列。Java併發包提供了豐富的佇列類,可以簡單分為:

  • 無鎖非阻塞併發佇列:ConcurrentLinkedQueue和ConcurrentLinkedDeque
  • 普通阻塞佇列:基於陣列的ArrayBlockingQueue,基於連結串列的LinkedBlockingQueue和LinkedBlockingDeque
  • 優先順序阻塞佇列:PriorityBlockingQueue
  • 延時阻塞佇列:DelayQueue
  • 其他阻塞佇列:SynchronousQueue和LinkedTransferQueue

無鎖非阻塞是這些佇列不使用鎖,所有操作總是可以立即執行,主要通過迴圈CAS實現併發安全,阻塞佇列是指這些佇列使用鎖和條件,很多操作都需要先獲取鎖或滿足特定條件,獲取不到鎖或等待條件時,會等待(即阻塞),獲取到鎖或條件滿足再返回。

這些佇列迭代都不會丟擲ConcurrentModificationException,都是弱一致的,後面就不單獨強調了。下面,我們來簡要探討每類佇列的用途、用法和基本實現原理。

無鎖非阻塞併發佇列

有兩個無鎖非阻塞佇列:ConcurrentLinkedQueue和ConcurrentLinkedDeque,它們適用於多個執行緒併發使用一個佇列的場合,都是基於連結串列實現的,都沒有限制大小,是無界的,與ConcurrentSkipListMap類似,它們的size方法不是一個常量運算,不過這個方法在併發應用中用處也不大。

ConcurrentLinkedQueue實現了Queue介面,表示一個先進先出的佇列,從尾部入隊,從頭部出隊,內部是一個單向連結串列。ConcurrentLinkedDeque實現了Deque介面,表示一個雙端佇列,在兩端都可以入隊和出隊,內部是一個雙向連結串列。它們的用法類似於LinkedList,我們就不贅述了。

這兩個類最基礎的原理是迴圈CAS,ConcurrentLinkedQueue的演算法基於一篇論文:"Simple, Fast, and Practical Non-Blocking and Blocking Concurrent Queue Algorithms",ConcurrentLinkedDeque擴充套件了ConcurrentLinkedQueue的技術,但它們的具體實現都非常複雜,我們就不探討了。

普通阻塞佇列

除了剛介紹的兩個佇列,其他佇列都是阻塞佇列,都實現了介面BlockingQueue,在入隊/出隊時可能等待,主要方法有:

//入隊,如果佇列滿,等待直到佇列有空間
void put(E e) throws InterruptedException;
//出隊,如果佇列空,等待直到佇列不為空,返回頭部元素
E take() throws InterruptedException;
//入隊,如果佇列滿,最多等待指定的時間,如果超時還是滿,返回false
boolean offer(E e, long timeout, TimeUnit unit) throws InterruptedException;
//出隊,如果佇列空,最多等待指定的時間,如果超時還是空,返回null
E poll(long timeout, TimeUnit unit) throws InterruptedException;
複製程式碼

普通阻塞佇列是常用的佇列,常用於生產者/消費者模式。

ArrayBlockingQueue和LinkedBlockingQueue都是實現了Queue介面,表示先進先出的佇列,尾部進,頭部出,而LinkedBlockingDeque實現了Deque介面,是一個雙端佇列。

ArrayBlockingQueue是基於迴圈陣列實現的,有界,建立時需要指定大小,且在執行過程中不會改變,這與我們在容器類中介紹的ArrayDeque是不同的,ArrayDeque也是基於迴圈陣列實現的,但是是無界的,會自動擴充套件。

LinkedBlockingQueue是基於單向連結串列實現的,在建立時可以指定最大長度,也可以不指定,預設是無限的,節點都是動態建立的。LinkedBlockingDeque與LinkedBlockingQueue一樣,最大長度也是在建立時可選的,預設無限,不過,它是基於雙向連結串列實現的。

內部,它們都是使用顯式鎖ReentrantLock顯式條件Condition實現的。

ArrayBlockingQueue的實現很直接,有一個陣列儲存元素,有兩個索引表示頭和尾,有一個變數表示當前元素個數,有一個鎖保護所有訪問,有兩個條件,"不滿"和"不空"用於協作,成員宣告如下:

final Object[] items;
int takeIndex; // 頭
int putIndex; //尾
int count; //元素個數
final ReentrantLock lock;
private final Condition notEmpty;
private final Condition notFull;
複製程式碼

實現思路與我們在72節實現的類似,就不贅述了。

與ArrayBlockingQueue類似,LinkedBlockingDeque也是使用一個鎖和兩個條件,使用鎖保護所有操作,使用"不滿"和"不空"兩個條件,LinkedBlockingQueue稍微不同,因為它使用連結串列,且只從頭部出隊、從尾部入隊,它做了一些優化,使用了兩個鎖,一個保護頭部,一個保護尾部,每個鎖關聯一個條件。

優先順序阻塞佇列

普通阻塞佇列是先進先出的,而優先順序佇列是按優先順序出隊的,優先順序高的先出,我們在容器類中介紹過優先順序佇列PriorityQueue及其背後的資料結構

PriorityBlockingQueue是PriorityQueue的併發版本,與PriorityQueue一樣,它沒有大小限制,是無界的,內部的陣列大小會動態擴充套件,要求元素要麼實現Comparable介面,要麼建立PriorityBlockingQueue時提供一個Comparator物件。

與PriorityQueue的區別是,PriorityBlockingQueue實現了BlockingQueue介面,在佇列為空時,take方法會阻塞等待。

另外,PriorityBlockingQueue是執行緒安全的,它的基本實現原理與PriorityQueue是一樣的,也是基於堆,但它使用了一個鎖ReentrantLock保護所有訪問,使用了一個條件協調阻塞等待。

延時阻塞佇列

延時阻塞佇列DelayQueue是一種特殊的優先順序佇列,它也是無界的,它要求每個元素都實現Delayed介面,該介面的宣告為:

public interface Delayed extends Comparable<Delayed> {
    long getDelay(TimeUnit unit);
}
複製程式碼

Delayed擴充套件了Comparable介面,也就是說,DelayQueue的每個元素都是可比較的,它有一個額外方法getDelay返回一個給定時間單位unit的整數,表示再延遲多長時間,如果小於等於0,表示不再延遲。

DelayQueue也是優先順序佇列,它按元素的延時時間出隊,它的特殊之處在於,只有當元素的延時過期之後才能被從佇列中拿走,也就是說,take方法總是返回第一個過期的元素,如果沒有,則阻塞等待。

DelayQueue可以用於實現定時任務,我們看段簡單的示例程式碼:

public class DelayedQueueDemo {
    private static final AtomicLong taskSequencer = new AtomicLong(0);

    static class DelayedTask implements Delayed {
        private long runTime;
        private long sequence;
        private Runnable task;

        public DelayedTask(int delayedSeconds, Runnable task) {
            this.runTime = System.currentTimeMillis() + delayedSeconds * 1000;
            this.sequence = taskSequencer.getAndIncrement();
            this.task = task;
        }

        @Override
        public int compareTo(Delayed o) {
            if (o == this) {
                return 0;
            }
            if (o instanceof DelayedTask) {
                DelayedTask other = (DelayedTask) o;
                if (runTime < other.runTime) {
                    return -1;
                } else if (runTime > other.runTime) {
                    return 1;
                } else if (sequence < other.sequence) {
                    return -1;
                } else {
                    return 1;
                }
            }
            throw new IllegalArgumentException();
        }

        @Override
        public long getDelay(TimeUnit unit) {
            return unit.convert(runTime - System.currentTimeMillis(),
                    TimeUnit.MICROSECONDS);
        }

        public Runnable getTask() {
            return task;
        }
    }

    public static void main(String[] args) throws InterruptedException {
        DelayQueue<DelayedTask> tasks = new DelayQueue<>();
        tasks.put(new DelayedTask(2, new Runnable() {
            @Override
            public void run() {
                System.out.println("execute delayed task");
            }
        }));

        DelayedTask task = tasks.take();
        task.getTask().run();
    }
}
複製程式碼

DelayedTask表示延時任務,只有延時過期後任務才會執行,任務按延時時間排序,延時一樣的按照入隊順序排序。

內部,DelayQueue是基於PriorityQueue實現的,它使用一個鎖ReentrantLock保護所有訪問,使用一個條件available表示頭部是否有元素,當頭部元素的延時未到時,take操作會根據延時計算需睡眠的時間,然後睡眠,如果在此過程中有新的元素入隊,且成為頭部元素,則阻塞睡眠的執行緒會被提前喚醒然後重新檢查。以上是基本思路,DelayQueue的實現有一些優化,以減少不必要的喚醒,具體我們就不探討了。

其他阻塞佇列

Java併發包中還有兩個特殊的阻塞佇列,SynchronousQueue和LinkedTransferQueue。

SynchronousQueue

SynchronousQueue與一般的佇列不同,它不算一種真正的佇列,它沒有儲存元素的空間,儲存一個元素的空間都沒有。它的入隊操作要等待另一個執行緒的出隊操作,反之亦然。如果沒有其他執行緒在等待從佇列中接收元素,put操作就會等待。take操作需要等待其他執行緒往佇列中放元素,如果沒有,也會等待。SynchronousQueue適用於兩個執行緒之間直接傳遞資訊、事件或任務。

LinkedTransferQueue

LinkedTransferQueue實現了TransferQueue介面,TransferQueue是BlockingQueue的子介面,但增加了一些額外功能,生產者在往佇列中放元素時,可以等待消費者接收後再返回,適用於一些訊息傳遞型別的應用中。TransferQueue的介面定義為:

public interface TransferQueue<E> extends BlockingQueue<E> {
    //如果有消費者在等待(執行take或限時的poll),直接轉給消費者,
    //返回true,否則返回false,不入隊
    boolean tryTransfer(E e);
    //如果有消費者在等待,直接轉給消費者,
    //否則入隊,阻塞等待直到被消費者接收後再返回
    void transfer(E e) throws InterruptedException;
    //如果有消費者在等待,直接轉給消費者,返回true
    //否則入隊,阻塞等待限定的時間,如果最後被消費者接收,返回true
    boolean tryTransfer(E e, long timeout, TimeUnit unit)
        throws InterruptedException;
    //是否有消費者在等待
    boolean hasWaitingConsumer();
    //等待的消費者個數
    int getWaitingConsumerCount();
}
複製程式碼

LinkedTransferQueue是基於連結串列實現的、無界的TransferQueue,具體實現比較複雜,我們就不探討了。

小結

本節簡要介紹了Java併發包中的各種佇列,包括其基本概念和基本原理。

73節到本節,我們介紹了Java併發包的各種容器,至此,就介紹完了,在實際開發中,應該儘量使用這些現成的容器,而非重新發明輪子。

Java併發包中還提供了一種方便的任務執行服務,使用它,可以將要執行的併發任務與執行緒的管理相分離,大大簡化併發任務和執行緒的管理,讓我們下一節來探討。

(與其他章節一樣,本節所有程式碼位於 github.com/swiftma/pro…)


未完待續,檢視最新文章,敬請關注微信公眾號“老馬說程式設計”(掃描下方二維碼),從入門到高階,深入淺出,老馬和你一起探索Java程式設計及計算機技術的本質。用心原創,保留所有版權。

計算機程式的思維邏輯 (76) - 併發容器 - 各種佇列

相關文章