Java 併發核心編程
內容涉及:
1、關於java併發
2、概念
3、保護共享數據
4、併發集合類
5線程
6、線程協作及其他
1、關於java併發
自從java創建以來就已經支持併發的理念,如線程和鎖。這篇指南主要是爲幫助java多線程開發人員理解併發的核心概念以及如何應用這些理念。本文的主題是關於具有java語言風格的Thread、synchronized、volatile,以及J2SE5中新增的概念,如鎖(Lock)、原子性(Atomics)、併發集合類、線程協作摘要、Executors。開發者通過這些基礎的接口可以構建高併發、線程安全的java應用程序。
2、概念
本部分描述的java併發概念在這篇DZone Refard會被通篇使用。
從JVM併發看CPU內存指令重排序(Memory Reordering):http://kenwublog.com/illustrate-memory-reordering-in-cpu
java內存模型詳解: http://kenwublog.com/explain-java-memory-model-in-detail
概念 |
描述 |
Java Memory Model Java內存模型 |
在JavaSE5(JSR133)中定義的Java Memory Model(JMM)是爲了確保當編寫併發代碼的時候能夠提供Java程序員一個可用的JVM實現。術語JMM的作用類似與一個觀察同步讀寫字段的monitor。它按照“happens-before order(先行發生排序)”的順序—可以解釋爲什麼一個線程可以獲得其他線程的結果,這組成了一個屬性同步的程序,使字段具有不變性,以及其他屬性。 |
monitor Monitor |
Java語言中,每個對象都擁有一個訪問代碼關鍵部分並防止其他對象訪問這段代碼的“monitor”(每個對象都擁有一個對代碼關鍵部分提供訪問互斥功能的“monitor”)。這段關鍵部分是使用synchronized對方法或者代碼標註實現的。同一時間在同一個monitor中,只允許一個線程運行代碼的任意關鍵部分。當一個線程試圖獲取代碼的關鍵部分時,如果這段代碼的monitor被其他線程擁有,那麼這個線程會無限期的等待這個monitor直到它被其他線程釋放。除了訪問互斥之外,monitor還可以通過wait和notify來實現協作。 |
原子字段賦值 Atomic field assignment |
除了doubles和longs之外的類型,給一個這些類型的字段賦值是一個原子操作。在JVM中,doubles和longs的更新是被實現爲2個獨立的操作,因此理論上可能會有其他的線程得到一個部分更新的結果。爲了保護共享的doubles和longs,可以使用volatile標記這個字段或者在synchronized修飾的代碼塊中操作字段。 |
競爭狀態 Race condition |
競爭發生在當不少於一個線程對一個共享的資源進行一系列的操作,如果這些線程的操作的順序不同,會導致多種可能的結果。 |
數據競爭 Data race |
數據競爭主要發生在多個線程訪問一個共享的、non-final、non-volatile、沒有合適的synchronization限制的字段。Java內存模型不會對這種非同步的數據訪問提供任何的保證。在不同的架構和機器中數據競爭會導致不可預測的行爲。 |
安全發佈 Safe publications |
在一個對象創建完成之前就發佈它的引用時非常危險的。避免這種使用這種引用的一種方法就是在創建期間註冊一個回調接口。另外一種不安全的情況就是在構造子中啓動一個線程。在這2種情況中,非完全創建的對象對於其他線程來說都是可見的。 |
不可變字段 Final Fields |
不可變字段在對象創建之後必須明確設定一個值,否則編譯器就會報出一個錯誤。一旦設定值後,不可變字段的值就不可以再次改變。將一個對象的引用設定爲不可變字段並不能阻止這個對象的改變。例如,ArrayList類型的不可變字段不能改變爲其他ArrayList實例的引用,但是可以在這個list實例中添加或者刪除對象。 在創建結尾,對象會遇到”final field freeze”:如果對象被安全的發佈後,即使在沒有synchronization關鍵字修飾的情況下,也能保證所有的線程獲取final字段在構建過程中設定的值。final field freezer不僅對final字段有用,而且作用於final對象中的可訪問屬性。 |
不可變對象 Immutable objects |
在語法上final 字段能夠創建不需要synchronization修飾的、能夠被共享讀取的線程安全的不可變對象。實現Immutable Object需要保證如下條件: ·對象被安全的發佈(在創建過程中this 引用是無法避免的) ·所有字段被聲明爲final ·在創建之後,在對象字段能夠被訪問的範圍中是不允許修改這個字段的。 ·class被聲明爲final(爲了防止subclass違反這些規則) |
3、保護共享數據
編寫線程安全的java程序,當修改共享數據的時候要求開發人員使用合適的鎖來保護數據。鎖能夠建立符合Java Memory Model要求的訪問順序,而且確保其他線程知道數據的變化。
注意:
在Java Memory Model中,如果沒有被synchronization修飾,改變數據不需要什麼特別的語法表示。JVM能夠自由地重置指令順序的特性和對可見性的限制方式很容易讓開發人員感到奇怪。
3.1、Synchronized
每個對象實例都擁有一個每次只能讓一個線程鎖住的monitor。synchronized能夠用在一個方法或者代碼塊中來鎖住這個monitor。用synchronized修飾一個對象,當修改這個對象的一個字段,synchronized保證其他線程餘下的對這個對象的讀操作能夠獲取修改後的值。需要注意的是修改同步塊之外的數據或者synchronized沒有修飾當前被修改的對象,那麼不能保證其他線程讀到這些最新的數據。synchronized關鍵字能夠修飾一個對象實例中的函數或者代碼塊。在一個非靜態方法中this關鍵字表示當前的實例對象。在一個synchronized修飾的靜態的方法中,這個方法所在的類使用Class作爲實例對象。
3.2、Lock
Java.util.concurrent.locks包中有個標準Lock接口。ReentrantLock 實現了Lock接口,它完全擁有synchronized的特性,同時還提供了新的功能:獲取Lock的狀態、非阻塞獲取鎖的方法tryLock()、可中斷Lock。
下面是使用ReentrantLock的詳細示例:
public class Counter{ private final Lock lock = new ReentrantLock(); private int value; public int increment() { lock.lock(); try { return ++value; }finally{ lock.unlock(); } } } |
3.3、ReadWriteLock
Java.util.concurrent.locks包中還有個ReadWriteLock接口(實現類是ReentrantWriteReadLock),它定義一對鎖:讀鎖和寫鎖,特徵是能夠被併發的讀取但每次只能有一個寫操作。使用ReentrantReadWriteLock併發讀取特性的詳細示例:
public class ReadWrite { private final ReadWriteLock lock = new ReentrantReadWriteLock(); private int value; public void increment(){ lock.writeLock().lock(); try{ value++; }finally{ lock.writeLock().unlock(); } } public int current(){ lock.readLock().lock(); try{ return value; }finally{ lock.readLock().unlock(); } } } |
3.4、volatile
volatile原理與技巧: http://kenwublog.com/the-theory-of-volatile
volatile修飾符用來標註一個字段,表明任何對這個字段的修改都必須能被其他隨後訪問的線程獲取到,這個修飾符和同步無關。因此,volatile修飾的數據的可見性和synchronization類似,但是這個它只作用於對字段的讀或寫操作。在JavaSE5之前,因爲JVM的架構和實現的原因,不同JVM的volatile效果是不同的而且也是不可信的。下面是Java內存模型明確地定義volatile的行爲:
public class Processor implements Runnable { private volatile boolean stop; public void stopProcessing(){ stop = true; } public void run() { while (!stop) { //do processing } } } |
注意:使用volatile修飾一個數組並不能讓這個數組的每個元素擁有volatile特性,這種聲明只是讓這個數組的reference具有volatile屬性。數組被聲明爲AtomicIntegerArray類型,則能夠擁有類似volatile的特性。
3.5、原子類
使用volatile的一個缺點是它能夠保證數據的可見性,卻不能在一個原子操作中對volatile修飾的字段同時進行校驗和更新操作。java.util.concurrent.atomic包中有一系列支持在單個非鎖定(lock)的變量上進行原子操作的類,類似於volatile。示例:
public class Counter{ private AtomicInteger value = new AtomicInteger(); private int value; public int increment() { return value.incrementAndGet(); } } |
incrementAndGet方法是原子類的複合操作的一個示例。booleans, integers, longs, object references, integers數組, longs數組, object references數組 都有相應的原子類。
3.6、ThreadLocal
通過ThreadLocal能數據保存在一個線程中,而且不需要lock同步。理論上ThreadLocal可以讓一個變量在每個線程都有一個副本。ThreadLocal常用來屏蔽線程的私有變量,例如“併發事務”或者其他的資源。而且,它還被用來維護每個線程的計數器,統計,或者ID生成器。
public class TransactionManager { private static final ThreadLocal<Transaction> currentTransaction = new ThreadLocal<Transaction>() { @Override protected Transaction initialValue() { return new NullTransaction(); } }; public Transaction currentTransaction() { Transaction current = currentTransaction.get(); if(current.isNull()) { current = new TransactionImpl(); currentTransaction.put(current); } return current; } } |
4、Concurrent Collections(併發集合類)
保護共享數據的一個關鍵技術是在存儲數據的類中封裝同步機制。所有對數據的使用都要經過同步機制的確認使這個技術能夠避免數據的不當訪問。在java.util.concurrent包中有很多爲併發使用情況下設計的數據結構。通常,使用這些數據結構比使用同步包裝器裝飾的非同步的集合的效率更高。
4.1、Concurrent lists and sets
在Table2 中列出了java.util.concurrent包中擁有的3個併發的List和Set實現類。
類 |
描述 |
CopyOnWriteArraySet |
CopyOnWriteArraySet在語意上提供寫時複製(copy-on-werite)的特性,對這個集合的每次修改都需要對當前數據結構新建一個副本,因此寫操作發費很大。在迭代器創建的時候,會對當前數據數據結構創建一個快照用於迭代。 |
CopyOnWriteArrayList |
CopyOnWriteArrayList和CopyOnWriteArraySet類似,也是基於copy-on-write語義實現了List接口 |
ConcurrentSkipListSet |
ConcurrentSkipListSet(在JavaSE 6新增的)提供的功能類似於TreeSet,能夠併發的訪問有序的set。因爲ConcurrentSkipListSet是基於“跳躍列表(skip list)”實現的,只要多個線程沒有同時修改集合的同一個部分,那麼在正常讀、寫集合的操作中不會出現競爭現象。 |
skip list: http://blog.csdn.net/yuanyufei/archive/2007/02/14/1509937.aspx
http://zh.wikipedia.org/zh-cn/%E8%B7%B3%E8%B7%83%E5%88%97%E8%A1%A8
4.2、Concurrent maps
Java.util.concurrent包中有個繼承Map接口的ConcurrentMap的接口,ConcurrentMap提供了一些新的方法(表3)。所有的這些方法在一個原子操作中各自提供了一套操作步驟。如果將每套步驟在放在map之外單獨實現,在非原子操作的多線程訪問的情況下會導致資源競爭。
表3:ConcurrentMap的方法:
方法 |
描述 |
putIfAbsent(K key, V value) : V |
如果key在map中不存在,則把key-value鍵值對放入map中,否則不執行任何操作。返回值爲原來的value,如果key不存在map中則返回null |
remove(Object key, Object value) : boolean |
如果map中有這個key及相應的value,那麼移除這對數據,否則不執行任何操作 |
replace (K key, V value) : V |
如果map中有這個key,那麼用新的value替換原來的value,否則不執行任何操作 |
replace (K key, V oldValue, V newValue) : boolean |
如果map中有這對key-oldValue數據,那麼用newValue替換原來的oldValue,否則不執行任何操作 |
在表4中列出的是ConcurrentMap的2個實現類
方法 |
描述 |
ConcurrentHashMap |
ConcurrentHashMap提供了2種級別的內部哈希方法。第一種級別是選擇一個內部的Segment,第二種是在選定的Segment中將數據哈希到buckets中。第一種方法通過並行地在不同的Segment上進行讀寫操作來實現併發。(ConcurrentHashMap是引入了Segment,每個Segment又是一個hash表,ConcurrentHashMap相當於是兩級Hash表,然後鎖是在Segment一級進行的,提高了併發性。http://mooncui.javaeye.com/blog/380884 http://www.javaeye.com/topic/344876 |
ConcurrentSkipListMap |
ConcurrentSkipListMap(JavaSE 6新增的類)功能類似TreeMap,是能夠被併發訪問的排序map。儘管能夠被多線程正常的讀寫---只要這些線程沒有同時修改map的同一個部分,ConcurrentSkipListMap的性能指標和TreeMap差不多。 |
4.3、Queues
Queues類似於溝通“生產者”和“消費者”的管道。組件從管道的一端放入,然後從另一端取出:“先進先出”(FIFO)的順序。Queue接口在JavaSE5新添加到java.util中的,能夠被用於單線程訪問的場景中,主要適用於多個生產者、一個或多個消費者的情景,所有的讀寫操作都是基於同一個隊列。
java.util.concurrent包中的BlockingQueue接口是Queue的子接口,而且還添加了新的特性處理如下場景:隊列滿(此時剛好有一個生產者要加入一個新的組件)、隊列空(此時剛好有一個消費者讀取或者刪除一個組件)。BlockingQueue提供如下方案解決這些情況:一直阻塞等待直到其他線程修改隊列的數據狀態;阻塞一段時間之後返回,如果在這段時間內有其他線程修改隊列數據,那麼也會返回。
表5:Queue和BlockingQueue的方法:
方法 |
策略 |
插入 |
移除 |
覈查 |
Queue |
拋出異常 |
add |
remove |
element |
返回特定的值 |
offer |
poll |
peek |
|
Blocking Queue |
一直阻塞 |
put |
take |
n/a |
超時阻塞 |
offer |
poll |
n/a |
在JDK中提供了一些Queue的實現,在表6中是這些實現類的關係列表。
方法 |
描述 |
PriorityQueue |
PriorityQueue是唯一一個非線程安全的隊列實現類,用於單線程存放數據並且將數據排序。 |
CurrentLinkedQueue |
一個無界的、基於鏈接列表的、唯一一個線程安全的隊列實現類,不支持BlockingQueue。 |
ArrayBlockingQueue |
一個有界的、基於數組的阻塞隊列。 |
LinkedBlockingQueue |
一個有界的、基於鏈接列表的阻塞隊列。有可能是最常用的隊列實現。 |
PriorityBlockingQueue |
一個無界的、基於堆的阻塞隊列。隊列根據設置的Comparator(比較器)來確定組件讀取、移除的順序(不是隊列默認的FIFO順序) |
DelayQueue |
一個無界的、延遲元素(每個延遲元素都會有相應的延遲時間值)的阻塞隊列實現。只有在延時期過了之後,元素才能被移除,而且最先被移除的是延時最先到期的元素。 |
SynchronousQueue |
一種0容量的隊列實現,生產者添加元素之後必須等待消費者移除後纔可以返回,反之依然。如果生產者和消費者2個線程同時訪問,那麼參數直接從生產者傳遞到消費者。經常用於線程之間的數據傳輸。 |
4.4、Deque
在JavaSE6中新增加了兩端都可以添加和刪除的隊列-Deque (發音"deck",not "dick"). Deques不僅可以從一端添加元素,從另一端移除,而且兩端都可以添加和刪除元素。如同BlockingQueue,BlockingDeque接口也爲阻塞等待和超時等待的特殊情況提供瞭解決方法。因爲Deque繼承Queue、BlockingDeque繼承BlockingQueue,下表中的方法都是可以使用的:
接口 |
頭或尾 |
策略 |
插入 |
移除 |
覈查 |
Queue |
Head |
拋出異常 |
addFirst |
removeFirst |
getFirst |
返回特定的值 |
offerFirst |
pollFirst |
peekFirst |
||
Tail |
拋出異常 |
addLast |
removeLast |
getLast |
|
返回特定的值 |
offerLast |
pollLast |
peekLast |
||
BlockingQueue |
Head |
一直阻塞 |
putFirst |
takeFirst |
n/a |
超時阻塞 |
offerFirst |
pollFirst |
n/a |
||
Tail |
一直阻塞 |
putLast |
takeLast |
n/a |
|
超時阻塞 |
offerLast |
pollLast |
n/a |
Deque的一個特殊應用場景是隻在一個端口進行添加、刪除、檢查操作--堆棧(first-in-last-out順序)。Deque接口提供了stack相同的方法:push(), pop()和peek(),這方法和addFirst(), removeFirst(), peekFirst()一一對應,可以把Deque的任何一個實現類當做堆棧使用。表6中是JDK中Deque和BlockingDeque的實現。注意Deque繼承Queue,BlockingDeque繼承自BlockingQueue。
表8:Deques
5、線程
在Java中,java.lang.Thread類是用來代表一個應用或者JVM線程。代碼是在某個線程類的上下文環境中執行的(使用Thread.currentThread()來獲取當前運行的線程)。
5.1、線程通訊
線程之間最簡單的通訊方式是一個線程直接調用另一個線程對象的方法。表9中列出的是線程之間可以直接交互的方法。
表9:線程協作方法
類 |
描述 |
LinkedList |
這個經常被用到的類在JavaSE6中有了新的改進-實現了Deque接口。在LinkedList中,可以使用標準的Deque方法來添加或者刪除list兩端的元素。LinkedList還可以被當做一個非同步的堆棧,用來替代同步的Stack類 |
ArrayDeque |
一個非同步的、支持無限隊列長度(根據需要動態擴展隊列的長度)的Deque實現類 |
LinkedBlockingDeque |
LinkeBlockingDeque是Deque實現中唯一支持併發的、基於鏈接列表、隊列長度可選的類。 |
線程方法 |
描述 |
start |
啓動一個線程實例,並且執行它的run() 方法。 |
join |
一直阻塞直到其他線程退出 |
interrupt |
中斷其他線程。線程如果在一個方法中被阻塞,會對interrupt操作做出迴應,並在這個方法執行的線程中拋出InterruptedException異常;否則線程的中斷狀態被設定。 |
stop, suspend, resume, destroy |
這些方法都被廢棄,不應該再使用了。因爲線程處理過程中狀態問題會導致危險的操作。相反,應該使用interrupt() 或者 volatile標示來告訴一個線程應該做什麼。 |
5.2、"未捕獲異常"處理器
線程能夠指定一個UncaughtExceptionHandler來接收任何一個導致線程非正常突然終止的未捕獲異常的通知。
5.3、死鎖
當存在多個線程(最少2個)等待對方佔有的資源,就會形成資源循環依賴和線程等待,產生死鎖。最常見的導致死鎖的資源是對象monitor,同時其他阻塞操作(例如wait/notify)也能導致死鎖。
很多新的JVM能夠檢測Monitor死鎖,並且可以將線程 dump中由信號(中斷信號)、jstack或者其他線程dump工具生成的死鎖原因顯示打印出來。
除了死鎖,線程之間還會出現飢餓(starvation)和活鎖(livelock). Starvation是因爲一個線程長時間佔有一個鎖導致其他的線程一直處於等待狀態無法進行下一步操作。Livelock是因爲線程發費大量的時間來協調資源的訪問或者檢測避免死鎖導致沒有一個線程真正的幹活。
6、線程協作
6.1、wait/notify
wait/notify關鍵字適用於一個線程通知另一個線程所需的條件狀態已就緒,最常用於線程在循環中休眠直到獲取特定條件的場景. 例如,一個線程一直等待直到隊列中有一個組件能夠處理;當組件添加到隊列時,另一個線程能夠通知這個等待的線程。
wait和notify的經典用法是:
Thread t = new Thread(runnable); t.setUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() { public void uncaughtException(Thread t, Throwable e) { // TODO get Logger and log uncaught exception } }); t.start();
|
public class Latch { private final Object lock = new Object(); private volatile boolean flag = false; public void waitTillChange(){ synchronized (lock) { while(!flag){ try { lock.wait(); } catch (InterruptedException e) { } } } } public void change(){ synchronized (lock) { flag = true; lock.notifyAll(); } } } |
在代碼中需要注意的重要地方是:
l wait、notify、notifyAll必須在synchronized修飾的代碼塊中執行,否則會在運行的時候拋出IllegalMonitorStateException異常
l 在循環語句wait的時候一定要設定循環的條件--這樣能夠避免wait開始之前,線程所需的條件已經被其他線程提供了卻依然開始此線程wait導致的時間消耗。同時,這種辦法還能夠保證你的代碼不被虛假的信息喚醒。
l 總是要保證在調用notify和notifyAll之前,能夠提供符合線程退出等待的條件。否則會出現即使線程接收到通知信息,卻不能退出循環等待的情況。
6.2、Condition
在JavaSE5中新添加了java.util.concurrent.locks.Condition接口。Condition不僅在API中實現了wait/notify語義,而且提供了幾個新的特性,例如:爲每個Lock創建多重Condition、可中斷的等待、訪問統計信息等。Condition是通過Lock示例產生的,示例:
public class LatchCondition { private final Lock lock = new ReentrantLock(); private final Condition condition = lock.newCondition(); private volatile boolean flag = false;
public void waitTillChange(){ lock.lock(); try{ while(!flag){ try { condition.await(); } catch (InterruptedException e) { } } }finally{ lock.unlock(); } } public void change(){ lock.lock(); try{ flag = true; condition.notifyAll(); }finally{ lock.unlock(); } } } |
6.3、Coordination classes
java.util.concurrent包中有幾個類適用於常見的多線程通訊。這幾個協作類適用範圍幾乎涵蓋了使用wait/notify和Condition最常見的場景,而且更安全、更易於使用。
CyclicBarrier
在CyclicBarrier初始化的時候指定參與者的數量。參與者調用awart()方法進入阻塞狀態直到參與者的個數達到指定數量,此時最後一個到達的線程執行預定的屏障任務,然後釋放所有的線程。屏障可以被重複的重置狀態。常用於協調分組的線程的啓動和停止。
CountDownLatch
需要指定一個計數才能初始化CountDownLatch。線程調用await()方法進入等待狀態知道計數變爲0。其他的線程(或者同一個線程)調用countDown()來減少計數。如果計數變爲0後是無法被重置的。常用於當確定數目的操作完成後,觸發數量不定的線程。
Semaphore
Semaphore維護一個“許可”集,能夠使用acquire()方法檢測這個“許可”集,在“許可”可用之前Semaphore會阻塞每個acquire訪問。線程能夠調用release()來返回一個許可。當Semaphore只有一個“許可”的時候,可當做一個互斥鎖來使用。
Exchanger
線程在Exchanger的exchange()方法上進行交互、原子操作的方式交換數據。功能類似於數據可以雙向傳遞的SynchronousQueue加強版。
7、任務執行
很多java併發程序需要一個線程池來執行隊列中的任務。在java.util.concurrent包中爲這種類型的任務管理提供了一種可靠的基本方法。
7.1、ExecutorService
Executor和易擴展的ExecutorService接口規定了用於執行任務的組件的標準。這些接口的使用者可以通過一個標準的接口使用各種具有不同行爲的實現類。
最通用的Executor接口只能訪問這種類型的可執行(Runnable)任務 :
void execute(Runnable command)
Executor子接口ExecutorService新加了方法,能夠執行:Runnable任務、Callable任務以及任務集合。
Future<?> submit(Runnable task)
Future<T> submit(Callable<T> task)
Future<T> submit(Runnable task, T result)
List<Future<T>> invokeAll (Collection<? extends Callable<T>> tasks)
List<Future<T>> invokeAll (Collection<? extends Callable<T>> tasks, long timeout, TimeUnit unit)
T invokeAny(Collection<? extends Callable<T>> tasks)
T invokeAny(Collection<? extends Callable<T>> tasks, long timeout, TimeUnit unit)
7.2、Callable and Future
Callable類似於Runnable,而且能夠返回值、拋出異常:
l V call() throws Exception;
在一個任務執行框架中提交一個Callable 任務,然後返回一個Future結果是很常見的。Future表示在將來的某個時刻能夠獲取到結果。Future提供能夠獲取結果或者阻塞直到結果可用的方法。任務運行之前或正在運行的時候,可以通過Future中的方法取消。
如果只是需要一個Runnable特性的Future(例如在Executor執行),可用使用FutureTask。FutureTask實現了Future和Runnable接口,可用提交一個Runnable類型任務,然後在調用部分使用這個Future類型的任務。
7.3、實現ExecutorService
ExecutorService最主要的實現類是ThreadPoolExecutor。這個實現類提供了大量的可配置特性:
l 線程池--設定常用線程數量(啓動前可選參數)和最大可用線程數量。
l 線程工廠--通過自定義的線程工廠生成線程,例如生成自定義線程名的線程。
l 工作隊列--指定隊列的實現類,實現類必須是阻塞的、可以是無界的或有界的。
l 被拒絕的任務--當隊列已經滿了或者是執行者不可用,需要爲這些情況指定解決策略。
l 生命週期中的鉤子--重寫擴展在任務運行之前或之後的生命週期中的關鍵點
l 關閉--停止已接受的任務,等待正在運行的任務完成後,關閉ThreadPoolExecutor。
ScheduledThreadPoolExecutor是ThreadPoolExecutor的一個子類,能夠按照定時的方式完成任務(而不是FIFO方式)。在java.util.Timer不是足夠完善的情況下,ScheduleThreadPoolExecutor具有強大的可適用性。
Executors類有很多靜態方法(表10)用於創建適用於各種常見情況的預先包裝的ExecutorService和ScheduleExecutorService實例
表10
方法 |
描述 |
newSingleThreadExecutor |
創建只有一個線程的ExecutorService |
newFixedThreadPool |
返回擁有固定數量線程的ExecutorService |
newCachedThreadPool |
返回一個線程數量可變的ExecutorService |
newSingleThreadScheduledExecutor |
返回只有一個線程的ScheduledExecutorService |
newScheduledThreadPool |
創建擁有一組核心線程的ScheduledExecutorService |
下面的例子是創建一個固定線程池,然後提交一個長期運行的任務:
在這個示例中提交任務到executor之後,代碼沒有阻塞而是立即返回。在代碼的最後一行調用get()方法會阻塞直到有結果返回。
ExecutorService幾乎涵蓋了所有應該創建線程對象或線程池的情景。在代碼中需要直接創建一個線程的時候,可以考慮通過Executor工廠創建的ExecutorService能否實現相同的目標;這樣做經常更簡單、更靈活。
7.4、CompletionService
除了常見的線程池和輸入隊列模式,還有一種常見的情況:爲後面的處理,每個任務生成的結果必須積累下來。CompletionService接口允許提交Callable和Runnable任務,而且還可以從任務隊列中獲取這些結果:(綠色部分和英文版不一樣,已和作者確認,英文版將take()和poll()方法混淆了)
l Future<V> take () -- 如果結果存在則獲取,否則直接返回
l Future<V> poll () -- 阻塞直到結果可用
l Future<V> poll (long timeout, TimeUnit unit) -- 阻塞直到timeout時間結束
ExecutorCompletionService是CompletionService的標準實現類。在ExecutorCompletionService的構成函數中需要一個Executor,ExecutorCompletionService提供輸入隊列和線程池。
8、Hot Tip
熱門信息:當設置線程池大小的時候,最好是基於當前應用所運行的機器擁有的邏輯處理器的數量。在java中,可用使用Runtime.getRuntime().availableProcessors()獲取這個值。在JVM的生命週期中,可用處理器的數目是可變的。
9、關於作者
Alex Miller是Terracotta Inc公司Java集羣開源產品的技術負責人,曾在BEA System和MetaMatrix工作,是MetaMatrix的首席架構師。他對Java、併發、分佈式系統、查詢語言和軟件設計感興趣。他的tweeter:@puredanger,blog:http://tect.puredanger.com,很喜歡在用戶組會議中發言。在St. Louis,Alex是Lambda Lounge小組的創建人,Lambda Lounge用戶組是爲了學習、動態語言、Strange Loop開發會議而創建的。
10、翻譯後記
開始閱讀英文版的時候,並沒有覺得文章中有什麼晦澀的地方。但是在翻譯之後,才發現將文中的意思清楚地表達出來也是個腦力活,有時一句話能夠懂得意思,卻是很難用漢語表達出來:“只可意會,不可言傳”--這也能解釋我當年高中作文爲啥每次只能拿40分(總分60)。在禪宗,師傅教弟子佛理,多靠弟子自身的明悟,故有當頭棒喝、醍醐灌頂之說。做翻譯卻不能這樣,總不能讓讀者對着滿篇的鳥文去琢磨明悟吧,須得直譯、意譯並用,梳理文字。
翻譯也是一個學習的過程。閱讀本文的時候會無意忽略自己以爲不重要的詞句,待到真正翻譯的時候,才發現自己一知半解、一竅不通,就只好Google之,翻譯完成後,也學了些知識,可謂是一箭雙鵰。
個人精力所限,翻譯中難免有不對的地方,望大家予以指正。