在JDK 1.5 中新增加了java.util.Queue介面,用以支援隊列的常見操作。
public interface Queue<E>extends Collection<E>
隊列除了基本的 Collection 操作外,隊列還提供其他的插入、提取和檢查操作。每個方法都存在兩種形式:一種拋出異常(操作失敗時),另一種返回一個特殊值(null 或false,具體取決於操作)。插入操作的後一種形式是用於專門為有容量限制的Queue 實現設計的;在大多數實現中,插入操作不會失敗。
|
拋出異常 |
返回特殊值 |
| 插入 |
add(e) |
offer(e) |
| 移除 |
remove() |
poll() |
| 檢查 |
element() |
peek() |
隊列通常(但並非一定)以 FIFO(先進先出)的方式排序各個元素。不過優先順序隊列和 LIFO 隊列(或堆棧)例外,前者根據提供的比較子或元素的自然順序對元素進行排序,後者按 LIFO(後進先出)的方式對元素進行排序。無論使用哪種排序方式,隊列的頭 都是調用remove() 或poll() 所移除的元素。在 FIFO 隊列中,所有的新元素都插入隊列的末尾。其他種類的隊列可能使用不同的元素放置規則。每個Queue 實現必須指定其順序屬性。
如果可能,offer 方法可插入一個元素,否則返回false。這與Collection.add 方法不同,該方法只能通過拋出未經檢查的異常使添加元素失敗。offer 方法設計用於正常的失敗情況,而不是出現異常的情況,例如在容量固定(有界)的隊列中。
remove() 和 poll() 方法可移除和返回隊列的頭。到底從隊列中移除哪個元素是隊列排序策略的功能,而該策略在各種實現中是不同的。remove() 和poll() 方法僅在隊列為空白時其行為有所不同:remove() 方法拋出一個異常,而poll() 方法則返回null。
element() 和 peek() 返回,但不移除,隊列的頭。
Queue 實現通常不允許插入 null 元素,儘管某些實現(如LinkedList)並不禁止插入null。即使在允許 null 的實現中,也不應該將null 插入到 Queue 中,因為 null 也用作poll 方法的一個特殊傳回值,表明隊列不包含元素。
JDK1.5中提供了Queue的子類,如下
All Known Implementing Classes:
AbstractQueue, ArrayBlockingQueue, ArrayDeque, ConcurrentLinkedQueue, DelayQueue, LinkedBlockingDeque, LinkedBlockingQueue, LinkedList, PriorityBlockingQueue, PriorityQueue, SynchronousQueue
AbstractQueue類
如你所見,java.util.LinkedList類實現了java.util.Queue介面,同樣,AbstractQueue也是這樣。AbstractQueue 類實現了java.util介面的一些方法(因此在它的名字中包含abstract)。而AbstractQueue將重點放在了實現offer,poll和peek方法上。另外使用一些已經提供的具體實現。
PriorityQueue類
在PriorityQueue中,當你添加元素到Queue中時,實現了自動排序。根據你使用的PriorityQueue的不同構造器,Queue元素的順序要麼基於他們的自然順序要麼通過PriorirtyQueue構造器傳入的Comparator來確定。下面的程式碼範例了PirorityQueue類的使用方法。在Queue的前邊是字串"Alabama"-由於元素在PriorityQueue中是按自然順序排列的(此例中是按字母表順序)。
PriorityQueue<String> priorityQueue = new PriorityQueue<String>();
priorityQueue.offer("Texas");
priorityQueue.offer("Alabama");
priorityQueue.offer("California");
priorityQueue.offer("Rhode Island");
int queueSize = priorityQueue.size();
for (int i =0; i< queueSize; i++)
{
System.out.println(priorityQueue.poll());
}
執行結果如下:
Alabama
California
Rhode Island
Texas
Queue各項按照自然順序-字母順序-來排列。
如上提到的,你可以建立你自己的Comparator類並提供給PirorityQueue。如此,你可以定義你自己的排序方式。在PriorityQueueComparatorUsageExample 類中可找到此方式,在其中使用了一個名為State的助手類。如你在下邊看到的,在類定義中,State只簡單地包含了一個名字和人口。
private String name;
private int population;
public State(String name, int population)
{
super();
this.name = name;
this.population = population;
}
public String getName()
{
return this.name;
}
public int getPopulation()
{
return this.population;
}
public String toString()
{
return getName() + " - " + getPopulation();
}
在PriorityQueueComparatorUsageExample中,Queue使用了java.util.Comparator的自訂實現來定義排列順序(如下)。
PriorityQueue<State> priorityQueue =
new PriorityQueue(6, new Comparator<State>()
{
public int compare(State a, State b)
{
System.out.println("Comparing Populations");
int populationA = a.getPopulation();
int populationB = b.getPopulation();
if (populationB>populationA)
return 1;
else if (populationB<populationA)
return -1;
else
return 0;
}
}
);
執行PriorityQueueComparatorUsageExample類後,添加到Queue中的State對象將按人口數量排放(從低到高)。
阻塞Queue
Queue通常限定於給定大小。迄今為止,通過Queue的實現你已經看到,使用offer或add方法enqueue Queue(並用remove或poll來dequeue Queue)都是假設如果Queue不能提供添加或移除操作,那麼你不需要等待程式執行。java.util.concurrent.BlockingQueue介面實現阻塞。它添加了put和take方法。舉一個例子可能更有用。
使用原來的producer/consumer關係來假定你的producer寫一個Queue(更特定是一個BlockingQueue)。你有一些consumer正從Queue中讀取,在一個有序的方式下,哪種方式是你希望看到的。基本上,每個consumer需要等待先於它並獲准從Queue中提取項目的前一個consumer。用程式構建此結構,先產生一個producer線程用於向一個Queue中寫資料,然後產生一些consumer線程從同一Queue中讀取資料。注意,線程會阻塞另一線程直到當前線程做完從Queue中提取一項的操作。
下面的代碼展示了類Producer寫BlockingQueue的過程。注意run方法中的對象(你有責任實現,因為你繼承了Thread)在等待了隨機數量的時間(範圍從100到500毫秒)後,被放進了BlockingQueue。放到Queue中的對象只是一些包含訊息產生時的時間的字串。
添加對象的實際工作是由如下語句實現的:
blockingQueue.put("Enqueued at: " + time)
put方法會拋出InterruptedException,因此,put操作需要被try...catch塊包圍,用來捕獲被拋出的異常(見Listing 1)。
從producer中提取訊息的是Consumer對象,它也繼承自Thread對象並因此要實現run方法(見Listing 2)。
Consumer類在設計上是類似於Producer類的。Consumer類使用take方法去從Queue中取出(即dequeue)訊息,而不是將訊息放到BlockingQueue中。如前所述,這需要等待到有什麼內容確實存在於Queue中時才發生。如果producer線程停止放置(即enqueue)對象到Queue中,那麼consumer將等待到Queue的項目有效為止。下面所示的TestBlockingQueue類,產生四個consumer線程,它們從BlockingQueue中嘗試提取對象。
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
public class TestBlockingQueue
{
public static void main(String args[])
{
BlockingQueue<String> blockingQueue = new LinkedBlockingQueue<String>();
Producer producer = new Producer(blockingQueue, System.out);
Consumer consumerA = new Consumer("ConsumerA", blockingQueue, System.out);
Consumer consumerB = new Consumer("ConsumerB", blockingQueue, System.out);
Consumer consumerC = new Consumer("ConsumerC", blockingQueue, System.out);
Consumer consumerD = new Consumer("ConsumerD", blockingQueue, System.out);
producer.start();
consumerA.start();
consumerB.start();
consumerC.start();
consumerD.start();
}
}
Figure 1. Consumer Threads: These threads dequeue messages from the BlockingQueue in the order that you spawned them.
下面一行建立BlockingQueue:
BlockingQueue<String> blockingQueue
= new LinkedBlockingQueue<String>();
注意,它使用BlockingQueue的LinkedBlockingQueue實現。這是因為BlockingQueue是一個抽象類別,你不能直接執行個體化它。你也可以使用ArrayBlockingQueueQueue類型。ArrayBlockingQueue使用一個數組作為它的存放裝置,而LinkedBlockingQueue使用一個LinkedList。ArrayBlockingQueue的容量是固定的。對於LinkedBlockingQueue,最大值可以指定;預設是無邊界的。本範例程式碼採用無邊界方式。
在類的執行期間,從Queue中讀取對象以順序方式執行(見下面例子的執行)。實際上,一個consumer線程阻塞其他訪問BlockingQueue的線程直到它可以從Queue中取出一個對象。
DelayQueue-我是/不是不完整的
在某些情況下,存放在Queue中的對象,在它們準備被取出之前,會需要被放在另一Queue中一段時間。這時你可使用java.util.concurrent.DelayQueue類,他實作類別BlockingQueue介面。DelayQueue需要Queue對象被駐留在Queue上一段指定時間。
我想用來證實它的現執行個體子(這可能是你非常渴望的)是關於鬆餅(muffins)。噢,Muffin對象(象我們正在談論的Java-沒有coffee雙關意圖)。假定你有一個DelayQueue並在其中放了一些Muffin對象。Muffin對象(如下所示)必須實現java.util.concurrent.Delayed介面,以便可被放在DelayQueue中。這個介面需要Muffin對象實現getDelay方法(如下所示)。getDelay方法,實際上聲明給多長時間讓對象儲存在DelayQueue中。當該方法返回的值變為0或小於0時,對象就準備完畢(或在本例子中,是烤制完畢)並允許被取出(見Listing 3)。
Muffin類也實現compareTo(java.util.concurrent.Delayed)方法。由於Delayed介面繼承自java.lang.Comparable類,這通過約定限制你要實現Muffin對象的bakeCompletion時間。
由於你不是真想去吃沒有完全烤熟的Muffin,因此,需要將Muffin放在DelayQueue中存放推薦的烤制時間。Listing 4,取自DelayQueueUsageExample類,展示了從DelayQueue中enqueue和dequeue Muffin對象。
如你所見,對Muffin對象的烤制時間是使用它的構造器設定的(構造器期望烤制時間是以秒計)。
如前所講,Muffin對象放到DelayQueue中是不允許被取出的,直到他的延時時間(又叫烤制時間)超期。元素被從Queue中取出基於最早的延時時間。在本例中,如果你有一些已經烤過的Muffin對象,他們將按他們已經等待多久而被取出(換句話說,最早被烤制的Muffin會在新烤制的Muffin之前被取出)。
SynchronousQueue
在Java 1.5中,另外一種阻塞Queue實現是SynchronousQueue。相當有趣的是,該Queue沒有內在容量。這是故意的,因為Queue意在用於傳遞目的。這意味著,在一個同步Queue結構中,put請求必須等待來自另一線程的給SynchronousQueue的take請求。同時,一個take請求必須等待一個來自另一線程的給SynchronousQueue的put請求。用程式來樣本此概念,可參見範例程式碼。類似於前邊的LinkedBlockingQueue例子,它包含一個consumer(SynchConsumer),見Listing 5。
Listing 5中的代碼使用SynchronousQueue類的poll(long timeout,TimeUnit unit)方法。此方法允許poll過程在厭倦等待另一消費線程寫SynchronousQueue之前等待一個指定時間(本例中是20秒)。
在Listing 6中的producer(SynchProducer)使用相似的offer(E o,long timeout, TimeUnit unit)方法去放置對象到SynchronousQueue中。使用此方法允許在厭倦等待另一線程去讀取SynchronousQueue之前等待一段時間(本例中為10秒)。
TestSynchQueue 展示了producer和consumer的動作:
import java.util.concurrent.SynchronousQueue;
import java.util.concurrent.LinkedBlockingQueue;
public class TestSynchQueue
{
public static void main(String args[])
{
SynchronousQueue<String> synchQueue = new SynchronousQueue<String>();
SynchProducer producer = new SynchProducer("ProducerA",synchQueue, System.out);
SynchConsumer consumerA = new SynchConsumer("ConsumerA", synchQueue, System.out);
consumerA.start();
producer.start();
}
}
當試圖明白隱藏在SynchronousQueue後面的概念時,要牢記這些Queue通常被使用在什麼地方。JavaDoc中關於同步Queue指出:
"它們[同步Queue]是適合於傳遞設計,在那裡運行在一個線程中的對象必須與運行在另外一個線程中的對象同步以便於交給它一些資訊,時間或任務。"