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,生產者和消費者進行的是無中介的直接交易,當生產者/消費者沒有找到合適的目標時,即會發生阻塞。但由於減少了環節,其整體性能在一些系統中可能更加適合。該方法同樣支持在構造時確定為公平/默認的非公平模式,如果是非公平模式,有可能會導致某些生產者/消費者飢餓。