Java容器:Stack,Queue,PriorityQueue和BlockingQueue


1. Stack

Java中Stack類繼承了Vector類,在其基礎上實現了了棧的功能。由於是直接繼承而非通過接口進行隱藏(如Queue雖然由LinkedList實現,但對其非隊列接口進行了隱藏),Java的Stack擁有Vector的所有方法並且繼承了其線程安全特性(所以也和Vector一樣在性能上有所損耗)。

在List的基礎上,Stack添加了以下方法:

  • push:向棧中壓入一個元素並返回該元素。
  • peek:獲取棧頂元素,棧為空拋出異常。
  • pop:獲取並彈出棧頂元素,棧為空拋出異常。
  • empty:同isEmpty()。
  • search:基於lastIndexOf()實現,返回搜索元素離棧頂的最近距離。

可見,Stack是一個古老的,並為了模擬棧的操作不惜重復實現同一函數的方法。當比較注重效率時,顯然基於LinkedList或者ArrayList對棧重新實現,會有更好的效果。

2. Queue

從繼承層次上看,Queue和List,Map,Set一樣,是直接繼承了Collections類的容器類,但是從其實現上看,Queue又可以視為一種特殊的List。原因是Queue的直接實現方法是LinkedList,並對其功能加以限制,僅暴露了Queue支持的功能:add(),remove(),element(),offer(),poll(),peek(),put(),take()。當然LinkedList這些功能對於List接口,也並非全部暴露。

Queue對象能夠使用以下方法操作:

  • add:隊列末尾添加一個元素,若隊列已滿拋出異常。

  • offer:隊列末尾添加一個元素,若隊列已滿返回false,成功返回true。另外,可以附加時間,時間單位參數設置超時。

  • remove:移除並返回隊列頭部元素,若隊列已空拋出異常。

  • poll:移除並返回隊列頭部元素,若隊列已空返回Null。另外,可以附加時間,時間單位參數設置超時。

  • element:返回隊列頭部元素,若隊列已空拋出異常。

  • peek:返回隊列頭部元素,若隊列已空返回Null。

需要注意的是,由於隊列中poll和peek操作以Null為標志,所以隊列中添加Null元素是不合法的。

3. PriorityQueue

在介紹完Stack和Queue后,自然要說另一個最基本的數據結構——Heap,也就是這里的PriorityQueue優先隊列。有關優先隊列,我在很久很久以前介紹過C++的優先隊列,Java也一樣,重點部分是對Comparator的操作。
由於知道PriorityQueue是基於Heap的,當新的元素存儲時,會調用siftUpUsingComparator方法,其定義如下:

    private void siftUpUsingComparator(int k, E x) {
        while (k > 0) {
            int parent = (k - 1) >>> 1;
            Object e = queue[parent];
            if (comparator.compare(x, (E) e) >= 0)
                break;
            queue[k] = e;
            k = parent;
        }
        queue[k] = x;
    }

我們從代碼可以看出,當Comparator的返回值為負時,會進行siftUp操作。例如,如果我們想讓數值大的數字(下例子中為a)先出列,則讓compare(a,b),則返回-1即可。可以看出來,和C++類似,方法取得的是“不小於”的概念。我們定義的方法可以視為是一個“小於”方法。

另外我們要注意到Comparator這個接口盡管有很多方法,但是有@FunctionalInterface標志,說明這是一個函數接口,換言之,在Java8中,我們可以使用lambda表達式來寫這個方法。

用匿名類方法寫的Comparator:

        PriorityQueue priorityQueue=new PriorityQueue(new Comparator<Student>() {
            @Override
            public int compare(Student student1, Student student2) {
                return student1.mark.compareTo(student2.mark);
            }
        });

用lambda表達式寫Comparator

        PriorityQueue priorityQueue=new PriorityQueue((Comparator<Student>)(student1, student2)->student1.mark.compareTo(student2.mark));

這里需要注意的是,如果不加上類型轉換,Java無法正確推斷lambda表達式的類型。

4. BlockingQueue

Java中Queue的最重要的應用大概就是其子類BlockingQueue了。

考慮到生產者消費者模型,我們有多個生產者和多個消費者,生產者不斷提供資源給消費者,但如果它們的生產/消費速度不匹配或者不穩定,則會造成大量的生產者閑置/消費者閑置。此時,我們需要使用一個緩沖區來存儲資源,即生產者將資源置於緩沖區,而消費者不斷地從緩沖區中取用資源,從而減少了閑置和阻塞。

BlockingQueue,阻塞隊列,即可視之為一個緩沖區應用於多線程編程之中。當隊列為空時,它會阻塞所有消費者線程,而當隊列為滿時,它會阻塞所有生產者線程。

在queue的基礎上,BlockingQueue又添加了以下方法:

  • put:隊列末尾添加一個元素,若隊列已滿阻塞。
  • take:移除並返回隊列頭部元素,若隊列已空阻塞。
  • drainTo:一次性獲取所有可用對象,可以用參數指定獲取的個數,該操作是原子操作,不需要針對每個元素的獲取加鎖。

BlockingQueue接口有以下幾種實現。

4.1. ArrayBlockingQueue

由一個定長數組和兩個標識首尾的整型index標識組成,生產者放入數據和消費者取出數據對於ArrayBlockingQueue而言使用了同一個鎖(一個私有的ReentrantLock),因而無法實現真正的並行。可以在初始化時除長度參數以外,附加一個boolean類型的變量,用於給其私有的ReentrantLock進行初始化(初始化是否為公平鎖,默認為false)。

4.2. LinkedBlockingQueue

LinkedBlockingQueue的最大特點是,若沒有指定最大容量,其可以視為無界隊列(有默認最大容量限制,往往系統資源耗盡也無法達到)。即,不對生產者的行為加以限制,只在隊列為空的時候限制消費者的行為。LinkedBlockingQueue采用了讀寫分離的兩個ReentrantLock去控制put和take,因而有了更好的性能(類似讀寫鎖提供讀寫場景下更好的性能),如下:

    /** 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();

ArrayBlockingQueue和LinkedBlockingQueue是最常用的兩種阻塞隊列。

4.3. PriorityBlockingQueue

PriorityBlockingQueue是對PriorityQueue的包裝,因而也是一個優先隊列。其優先級默認是直接比較,大者先出隊,也可以從構造器傳入自定義的Comparator。由於PriorityQueue從實現上是一個無界隊列,PriorityBlockingQueue同樣是一個無界隊列,對生產者不做限制。

4.4. DelayQueue

DelayQueue是在PriorityBlockingQueue的基礎上包裝產生的,它用於存放Delayed對象,該隊列的頭部是延遲期滿后保存時間最長的Delayed元素(即,以時間為優先級利用PriorityBlockingQueue),當沒有元素延遲期滿時,對其進行poll操作將會返回Null。take操作會阻塞。

4.5. SynchronousQueue

SynchronousQueue十分特殊,它沒有容量——換言之,它是一個長度為0的BlockingQueue,生產者和消費者進行的是無中介的直接交易,當生產者/消費者沒有找到合適的目標時,即會發生阻塞。但由於減少了環節,其整體性能在一些系統中可能更加適合。該方法同樣支持在構造時確定為公平/默認的非公平模式,如果是非公平模式,有可能會導致某些生產者/消費者飢餓。

5. 參考文章

Java多線程-工具篇-BlockingQueue


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM