java集合類(並發集合) 三

來源:互聯網
上載者:User

在 Java 編程的早期階段,位於 Oswego 市的紐約州立大學(SUNY) 的一位教授決定建立一個簡單的庫,以協助開發人員構建可以更好地處理多線程情況的應用程式。這並不是說用現有的庫就不能實現,但是就像有了標準網路程式庫一樣,用經過調試的、可信任的庫更容易自己處理多線程。在 Addision-Wesley 的一本相關書籍的協助下,這個庫變得越來越流行了。最終,作者 Doug Lea 決定設法讓它成為 Java 平台的標準部分 —— JSR-166。這個庫最後變成了 Tiger 版本的
java.util.concurrent 包。在這篇新的 馴服 Tiger 技巧中,我們將探討 Collection Framework 中新的
Queue 介面、這個介面的非並發和並發實現、並發 Map 實現和專用於讀操作大大超過寫操作這種情況的並發
List
Set 實現。

介紹 Queue 介面

java.util 包為集合提供了一個新的基本介面: java.util.Queue 。雖然肯定可以在相對應的兩端進行添加和刪除而將
java.util.List 作為隊列對待,但是這個新的 Queue 介面提供了支援添加、刪除和檢查集合的更多方法,如下所示:

public boolean offer(Object element)public Object remove()public Object poll()public Object element()public Object peek()

基本上,一個隊列就是一個先入先出(FIFO)的資料結構。一些隊列有大小限制,因此如果想在一個滿的隊列中加入一個新項,多出的項就會被拒絕。這時新的
offer
方法就可以起作用了。它不是對調用 add() 方法拋出一個 unchecked 異常,而只是得到由
offer()
返回的 false。 remove()poll() 方法都是從隊列中刪除第一個元素(head)。
remove() 的行為與 Collection 介面的版本相似,但是新的 poll() 方法在用空集合調用時不是拋出異常,只是返回 null。因此新的方法更適合容易出現異常條件的情況。後兩個方法
element()peek() 用於在隊列的頭部查詢元素。與 remove() 方法類似,在隊列為空白時,
element() 拋出一個異常,而 peek() 返回 null。

使用基本隊列

在 Tiger 中有兩組 Queue 實現:實現了新 BlockingQueue 介面的和沒有實現這個介面的。我將首先分析那些沒有實現的。

在最簡單的情況下,原來有的 java.util.LinkedList 實現已經改造成不僅實現 java.util.List 介面,而且還實現
java.util.Queue 介面。可以將集合看成這兩者中的任何一種。清單 1 顯示將 LinkedList 作為
Queue 使用的一種方法:

清單 1. 使用 Queue 實現

Queue queue = new LinkedList();  queue.offer("One");  queue.offer("Two");  queue.offer("Three");  queue.offer("Four");  // Head of queue should be One  System.out.println("Head of queue is: " + queue.poll());

再複雜一點的是新的 java.util.AbstractQueue 類。這個類的工作方式類似於 java.util.AbstractList
java.util.AbstractSet 類。在建立自訂集合時,不用自己實現整個介面,只是繼承抽象實現並填入細節。使用
AbstractQueue
時,必須為方法 offer()poll()peek() 提供實現。像
add()addAll() 這樣的方法修改為使用 offer() ,而
clear()
remove() 使用 poll() 。最後, element() 使用
peek() 。當然可以在子類中提供這些方法的最佳化實現,但是不是必須這麼做。而且,不必建立自己的子類,可以使用幾個內建的實現, 其中兩個是不阻塞隊列:
PriorityQueueConcurrentLinkedQueue

PriorityQueueConcurrentLinkedQueue 類在 Collection Framework 中加入兩個具體集合實現。
PriorityQueue 類實質上維護了一個有序列表。加入到 Queue 中的元素根據它們的天然排序(通過其
java.util.Comparable 實現)或者根據傳遞給建構函式的 java.util.Comparator 實現來定位。將清單 2 中的
LinkedList 改變為 PriorityQueue 將會列印出 Four 而不是 One,因為按字母排列 —— 字串的天然順序 —— Four 是第一個。
ConcurrentLinkedQueue 是基於連結節點的、安全執行緒的隊列。並發訪問不需要同步。因為它在隊列的尾部添加元素並從頭部刪除它們,所以只要不需要知道隊列的大小,
ConcurrentLinkedQueue 對公用集合的共用訪問就可以工作得很好。收集關於隊列大小的資訊會很慢,需要遍曆隊列。

使用阻塞隊列

新的 java.util.concurrent 包在 Collection Framework 中可用的具體集合類中加入了
BlockingQueue
介面和五個阻塞隊列類。假如不熟悉阻塞隊列概念,它實質上就是一種帶有一點扭曲的 FIFO 資料結構。不是立即從隊列中添加或者刪除元素,線程執行操作阻塞,直到有空間或者元素可用。
BlockingQueue 介面的 Javadoc 給出了阻塞隊列的基本用法,如清單 2 所示。生產者中的 put() 操作會在沒有空間可用時阻塞,而消費者的
take() 操作會在隊列中沒有任何東西時阻塞。

清單 2. 使用 BlockingQueue

class Producer implements Runnable {   private final BlockingQueue queue;   Producer(BlockingQueue q) { queue = q; }   public void run() {     try {       while(true) { queue.put(produce()); }     } catch (InterruptedException ex) { ... handle ...}   }   Object produce() { ... } } class Consumer implements Runnable {   private final BlockingQueue queue;   Consumer(BlockingQueue q) { queue = q; }   public void run() {     try {       while(true) { consume(queue.take()); }     } catch (InterruptedException ex) { ... handle ...}   }   void consume(Object x) { ... } } class Setup {   void main() {     BlockingQueue q = new SomeQueueImplementation();     Producer p = new Producer(q);     Consumer c1 = new Consumer(q);     Consumer c2 = new Consumer(q);     new Thread(p).start();     new Thread(c1).start();     new Thread(c2).start();   } }

五個隊列所提供的各有不同:

  • ArrayBlockingQueue :一個由數組支援的有界隊列。
  • LinkedBlockingQueue :一個由連結節點支援的可選有界隊列。
  • PriorityBlockingQueue :一個由優先順序堆支援的無界優先順序隊列。
  • DelayQueue :一個由優先順序堆支援的、基於時間的調度隊列。
  • SynchronousQueue :一個利用 BlockingQueue 介面的簡單聚集(rendezvous)機制。

前兩個類 ArrayBlockingQueueLinkedBlockingQueue 幾乎相同,只是在備份存放區器方面有所不同,
LinkedBlockingQueue 並不總是有容量界限。無大小界限的 LinkedBlockingQueue 類在添加元素時永遠不會有阻塞隊列的等待(至少在其中有
Integer.MAX_VALUE 元素之前不會)。

PriorityBlockingQueue 是具有無界限容量的隊列,它利用所包含元素的 Comparable 排序次序來以邏輯順序維護元素。可以將它看作
TreeSet 的可能替代物。例如,在隊列中加入字串 One、Two、Three 和 Four 會導致 Four 被第一個取出來。對於沒有天然順序的元素,可以為建構函式提供一個
Comparator 。不過對 PriorityBlockingQueue 有一個技巧。從 iterator() 返回的
Iterator 執行個體不需要以優先順序順序返回元素。如果必須以優先順序順序遍曆所有元素,那麼讓它們都通過 toArray() 方法並自己對它們排序,像
Arrays.sort(pq.toArray())

新的 DelayQueue 實現可能是其中最有意思(也是最複雜)的一個。加入到隊列中的元素必須實現新的 Delayed 介面(只有一個方法 ——
long getDelay(java.util.concurrent.TimeUnit unit) )。因為隊列的大小沒有界限,使得添加可以立即返回,但是在延遲時間過去之前,不能從隊列中取出元素。如果多個元素完成了延遲,那麼最早失效/失效時間最長的元素將第一個取出。實際上沒有聽上去這樣複雜。清單 3 示範了這種新的阻塞隊列集合的使用:

清單 3. 使用 DelayQueue 實現

import java.util.*;import java.util.concurrent.*;public class Delay {  /**   * Delayed implementation that actually delays   */  static class NanoDelay implements Delayed {     long trigger;    NanoDelay(long i) {       trigger = System.nanoTime() + i;    }    public int compareTo(Object y) {      long i = trigger;      long j = ((NanoDelay)y).trigger;      if (i < j) return -1;      if (i > j) return 1;      return 0;    }    public boolean equals(Object other) {      return ((NanoDelay)other).trigger == trigger;    }    public boolean equals(NanoDelay other) {      return ((NanoDelay)other).trigger == trigger;    }    public long getDelay(TimeUnit unit) {      long n = trigger - System.nanoTime();      return unit.convert(n, TimeUnit.NANOSECONDS);    }    public long getTriggerTime() {      return trigger;    }    public String toString() {      return String.valueOf(trigger);    }  }  public static void main(String args[]) throws InterruptedException {    Random random = new Random();    DelayQueue queue = new DelayQueue();    for (int i=0; i < 5; i++) {      queue.add(new NanoDelay(random.nextInt(1000)));    }    long last = 0;    for (int i=0; i < 5; i++) {      NanoDelay delay = (NanoDelay)(queue.take());      long tt = delay.getTriggerTime();      System.out.println("Trigger time: " + tt);      if (i != 0) {        System.out.println("Delta: " + (tt - last));      }      last = tt;    }  }}

這個例子首先是一個內部類 NanoDelay ,它實質上將暫停給定的任意納秒(nanosecond)數,這裡利用了 System 的新
nanoTime() 方法。然後 main() 方法只是將 NanoDelay 對象放到隊列中並再次將它們取出來。如果希望隊列項做一些其他事情,就需要在
Delayed 對象的實現中加入方法,並在從隊列中取出後調用這個新方法。(請隨意擴充 NanoDelay 以實驗加入其他方法做一些有趣的事情。)顯示從隊列中取出元素的兩次調用之間的時間差。如果時間差是負數,可以視為一個錯誤,因為永遠不會在延遲時間結束後,在一個更早的觸發時間從隊列中取得項。

SynchronousQueue 類是最簡單的。它沒有內部容量。它就像線程之間的手遞手機制。在隊列中加入一個元素的生產者會等待另一個線程的消費者。當這個消費者出現時,這個元素就直接在消費者和生產者之間傳遞,永遠不會加入到阻塞隊列中。

使用 ConcurrentMap 實現

新的 java.util.concurrent.ConcurrentMap 介面和 ConcurrentHashMap 實現只能在鍵不存在時將元素加入到 map 中,只有在鍵存在並映射到特定值時才能從 map 中刪除一個元素。

有一個新的 putIfAbsent() 方法用於在 map 中進行添加。這個方法以要添加到 ConcurrentMap 實現中的鍵的值為參數,就像普通的
put() 方法,但是只有在 map 不包含這個鍵時,才能將鍵加入到 map 中。如果 map 已經包含這個鍵,那麼這個鍵的現有值就會保留。
putIfAbsent() 方法是原子的。如果不調用這個原子操作,就需要從適當的同步塊中調用清單 4 中的代碼:

清單 4. 等價的 putIfAbsent() 代碼

 if (!map.containsKey(key)) {    return map.put(key, value);  } else {    return map.get(key);  }

使用 CopyOnWriteArrayList 和 CopyOnWriteArraySet

在 Doug Lea 的 Concurrent Programming in Java一書的第 2 章第 2.4.4 節(請參閱
參考資料)中,對 copy-on-write 模式作了最好的描述。實質上,這個模式聲明了,為了維護對象的一致性快照,要依靠不可變性(immutability)來消除在協調讀取不同的但是相關的屬性時需要的同步。對於集合,這意味著如果有大量的讀(即
get() ) 和迭代,不必同步操作以照顧偶爾的寫(即 add() )調用。對於新的 CopyOnWriteArrayList
CopyOnWriteArraySet 類,所有可變的(mutable)操作都首先取得後台數組的副本,對副本變更,然後替換副本。這種做法保證了在遍曆自身更改的集合時,永遠不會拋出
ConcurrentModificationException 。遍曆集合會用原來的集合完成,而在以後的操作中使用更新後的集合。

這些新的集合, CopyOnWriteArrayListCopyOnWriteArraySet ,最適合於讀操作通常大大超過寫操作的情況。一個最常提到的例子是使用監聽器列表。已經說過,Swing 組件還沒有改為使用新的集合。相反,它們繼續使用
javax.swing.event.EventListenerList 來維護它們的監聽器列表。

如清單 6 所示,集合的使用與它們的非 copy-on-write 替代物完全一樣。只是建立集合并在其中加入或者刪除元素。即使對象加入到了集合中,原來的
Iterator
也可以進行,繼續遍曆原來集合中的項。

清單 6. 展示一個 copy-on-write 集合

import java.util.*;import java.util.concurrent.*;public class CopyOnWrite {  public static void main(String args[]) {    List list1 = new CopyOnWriteArrayList(Arrays.asList(args));    List list2 = new ArrayList(Arrays.asList(args));    Iterator itor1 = list1.iterator();    Iterator itor2 = list2.iterator();    list1.add("New");    list2.add("New");    try {      printAll(itor1);    } catch (ConcurrentModificationException e) {      System.err.println("Shouldn't get here");    }    try {      printAll(itor2);    } catch (ConcurrentModificationException e) {      System.err.println("Will get here.");    }  }  private static void printAll(Iterator itor) {    while (itor.hasNext()) {      System.out.println(itor.next());    }  }}

這個樣本程式用命令列參數建立 CopyOnWriteArrayListArrayList 這兩個執行個體。在得到每一個執行個體的
Iterator 後,分別在其中加入一個元素。當 ArrayList 迭代因一個 ConcurrentModificationException 問題而立即停止時,
CopyOnWriteArrayList 迭代可以繼續,不會拋出異常,因為原來的集合是在得到 iterator 之後改變的。如果這種行為(比如通知原來一組事件監聽器中的所有元素)是您需要的,那麼最好使用 copy-on-write 集合。如果不使用的話,就還用原來的,並保證在出現異常時對它進行處理。

結束語

在 J2SE 平台的 Tiger 版中有許多重要的增加。除了語言層級的改變,如一般性支援,這個庫也許是最重要的增加了,因為它會被最廣泛的使用者使用。不要忽視加入到平台中的其他包,像 Java Management Extensions (JMX),但是大多數其他重要的庫增強只針對範圍很窄的開發人員。但是這個庫不是。除了用於鎖定和原子操作的其他並發公用程式,這些類也會經常使用。儘早學習它們並利用它們所提供的功能。

原文地址:http://www.ibm.com/developerworks/cn/java/j-tiger06164/#main

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.