第一章:簡介
1.1 併發簡史
促使進程出現的因素:資源利用率、公平性以及便利性等。這些因素同樣也促使着線程的出現。線程允許同一個進程中同時存在多個程序控制流。線程會共享進程範圍內的資源,例如內存句柄和文件句柄,但每個線程都有自己的程序計數器、棧以及局部變量等。在同一個程序中的多個線程也可以被同時調度到多個CPU上運行。
線程也被稱爲輕量級進程。線程是現代操作系統中基本的調度單位。
1.2 線程的優勢
- 發揮多處理器的強大能力。(使用多個線程也有助於在單處理器系統上獲得更高的吞吐率)
- 建模的簡單性。
- 異步事件的簡化處理。
- 響應更靈敏的用戶界面。
1.3 線程帶來的風險
- 安全性問題。
- 活躍性問題。
- 性能問題。
1.4 線程無處不在
框架通過在框架線程中調用應用程序代碼將併發性引入到程序中。在代碼中將不可避免地訪問應用程序狀態,因此所有訪問這些狀態的代碼路徑都必須是線程安全的。
第一部分:基礎知識
第二章:線程安全性
如果當多個線程訪問同一個可變的狀態變量時沒有使用合適的同步, 那麼程序就會出現錯誤。有三種方式可以修復這個問題:
- 不在線程之間共享該狀態變量。
- 將狀態變量修改爲不可變的變量。
- 在訪問狀態變量時使用同步。
在編寫併發應用程序時,一種正確的編程方法就是:首先使代碼正確運行,然後再提高代碼的速度。
2.1 什麼是線程安全性
線程安全性:當多個線程訪問某個類時,這個類始終都能表現出正確的行爲,那麼就稱這個類是線程安全的。
當多個線程訪問某個類時,不管運行時環境採用何種調度方式或者這些線程將如何交替執行,並且在主調代碼中不需要任何額外的同步或者協同,這個類都能表現出正確的行爲,那麼就稱這個類是線程安全的。
在線程安全類中封裝了必要的同步機制, 因此客戶端無須進一步採取同步措施。
無狀態的對象既不包含任何域,也不包含任何對其他類中域的引用。無狀態對象一定是線程安全的。
2.2 原子性
競態條件:由於不恰當的執行時序而出現不正確的結果。
競態條件的類型之一,”先檢查後執行”:首先觀察到某個條件爲真(例如文件X不存在),然後根據這個觀察結果採用相應的動作(創建文件X),但事實上,在你觀察到這個結果以及開始創建文件之間,觀察結果可能變得無效(另一個線程在這期間創建了文件X),從而導致各種問題(未預期的異常、數據被覆蓋、文件被破壞等)。
假定有兩個操作A和B,如果從執行A的線程來看,當另一個線程執行B時,要麼將B全部執行完,要麼完全不執行B,那麼A和B對彼此來說都是原子的。原子操作是指,對於訪問同一個狀態的所有操作(包括該操作本身)來說,這個操作是一個以原子方式執行的操作。
複合操作:包含了一組必須以原子方式執行的操作以確保線程安全性。
當在無狀態的類中添加”一個“狀態時,如果該狀態完全由線程安全的對象來管理,那麼這個類仍然是線程安全的。然而,當狀態變量的數量由一個變爲多個時,並不會像狀態變量數量由零個變爲一個那樣簡單。
在實際情況中,應儘可能地使用現有的線程安全對象(例如AtomicLong)來管理類的狀態。與非線程安全的對象相比,判斷線程安全對象的可能狀態及其狀態轉換情況要更爲容易,從而也更容易維護和驗證線程安全性。
2.3 加鎖機制
要保持狀態的一致性,就需要在單個原子操作中更新所有相關的狀態變量。
內置鎖:
synchronized (lock){
//訪問或修改由鎖保護的共享狀態
}
重入:由於內置鎖是可重入的,因此如果某個線程試圖獲得一個已經由它自己持有的鎖,那麼這個請求就會成功。“重入”意味着獲取鎖的操作的粒度是“線程”,而不是“調用”。重入的一種實現方法是,爲每個鎖關聯一個獲取計數值和一個所有者線程。
2.4 用鎖來保護狀態
對於可能被多個線程同時訪問的可變狀態變量,在訪問它時都需要持有同一個鎖,在這種情況下,我們稱狀態變量是由這個鎖保護的。
每個共享的和可變的變量都應該只由同一個鎖來保護,從而使維護人員知道是哪一個鎖。
對於每個包含多個變量的不變性條件,其中涉及的所有變量都需要由同一個鎖來保護。
雖然synchronized方法可以確保單個操作的原子性,但如果要把多個操作合併爲一個複合操作,還是需要額外的加鎖機制。
2.5 活躍性與性能
通常,在簡單性與性能之間存在着相互制約因素。當實現某個同步策略時,一定不要盲目地爲了性能而犧牲簡單性(這可能會破壞安全性)。
當執行時間較長的計算或者可能無法快速完成的操作時(例如,網絡I/O或控制檯I/O),一定不要持有鎖。
第三章:對象的共享
3.1 可見性
在沒有同步的情況下,編譯器、處理器以及運行時等都可能對操作的執行順序進行一些意想不到的調整。在缺乏足夠同步的多線程程序中,要想對內存操作的執行順序進行判斷,幾乎無法得到正確的結論。
只要有數據在多個線程之間共享,就使用正確的同步。
在多線程程序中使用共享且可變的long和double等類型的變量是不安全的(非原子的64位操作),除非用關鍵字volatile來聲明它們,或者用鎖保護起來。
加鎖的含義不僅僅侷限於互斥行爲,還包括內在可見性。爲了確保所有線程都能看到共享變量的最新值,所有執行讀操作或者寫操作的線程都必須在同一個鎖上同步。
從內存可見性的角度來看,寫入volatile變量相當於退出同步代碼塊,而讀取volatile變量就相當於進入同步代碼塊。
volatile boolean asleep;//當其他線程修改asleep時,執行判斷的線程可及時發現
...
while(!asleep)
countSomeSheep();//數綿羊
調試小提示:對於服務器應用程序,無論在開發階段還是在測試階段,當啓動JVM時一定都要指定-server命令行選項。server模式的JVM將比client模式的JVM進行更多的優化,例如將循環中未被修改的變量提升到循環外部,因此在開發環境(client模式的JVM)中能正確運行的代碼,可能會在部署環境(server模式的JVM)中運行失敗。例如,如果在以上程序中忘記把asleep變量聲明爲volatile類型,那麼server模式的JVM會將asleep變量的判斷條件提升到循環體外部(當把變量聲明爲volatile類型後,編譯器與運行時都會注意到這個變量是共享的,因此不會將該變量上的操作與其他內存操作一起重排序),這將導致一個無限循環,但client模式的JVM不會這麼做。在開發環境中出現無限循環問題時,解決這個問題的開銷遠小於解決在應用環境出現無限循環的開銷。
加鎖機制既可以確保可見性又可以確保原子性,而volatile變量只能確保可見性。
當且僅當滿足以下所有條件時, 才應該使用volatile變量:
- 對變量的寫入操作不依賴於變量的當前值,或者你能確保只有單個線程更新變量的值。
- 該變量不會與其他狀態變量一起納入不變性條件中。
- 在訪問變量時不需要加鎖。
3.2 發佈與逸出
“發佈(Publish)”一個對象的意思是指,使對象能夠在當前作用域之外的代碼中使用。當某個不應該發佈的對象被髮布時,這種情況就被稱爲逸出(Escape)。
不要在構造過程中使this引用逸出。當且僅當對象的構造函數返回時,對象才處於可預測的和一致的狀態。因此,當從對象的構造函數中發佈對象時,只是發佈了一個尚未構造完成的對象。
在構造過程中使this引用逸出的常見錯誤:
- 在構造函數中啓動一個線程。
- 在構造函數中調用一個可改寫的實例方法(既不是私有方法,也不是終結方法)。
如果想在構造函數中註冊一個事件監聽器或啓動線程,那麼可以使用一個私有的構造函數和一個公共的工廠方法,從而避免不正確的構造過程。
/**
* ThisEscape
* <p/>
* Implicitly allowing the this reference to escape
*/
public class ThisEscape {
public ThisEscape(EventSource source) {
source.registerListener(new EventListener() {
public void onEvent(Event e) {
doSomething(e);
}
});
}
}
/**
* SafeListener
* <p/>
* Using a factory method to prevent the this reference from escaping during construction
*/
public class SafeListener {
private final EventListener listener;
private SafeListener() {
listener = new EventListener() {
public void onEvent(Event e) {
doSomething(e);
}
};
}
public static SafeListener newInstance(EventSource source) {
SafeListener safe = new SafeListener();
source.registerListener(safe.listener);
return safe;
}
}
3.3 線程封閉
線程封閉:僅在單線程內訪問數據(不共享數據)。
- Ad-hoc線程封閉:維護線程封閉性的職責完全由程序實現來承擔。
- 棧封閉:只能通過局部變量才能訪問對象。
維持線程封閉性的一種更規範方法是使用ThreadLocal,這個類能使線程中的某個值與保存值的對象關聯起來。
ThreadLocal變量類似於全局變量,它能降低代碼的可重用性,並在類之間引入隱含的耦合性,因此在使用時要格外小心。
3.4 不變性
不可變對象一定是線程安全的。
不可變性並不等於將對象中所有的域都聲明爲final類型,即使對象中所有的域都是final類型的,這個對象仍然是可變的,因爲在final類型的域中可以保存對可變對象的引用。
當滿足以下條件時,對象纔是不可變的:
- 對象創建以後其狀態就不能修改。
- 對象的所有域都是final類型。
- 對象是正確創建的(在對象的創建期間,this引用沒有逸出)。
正如“除非需要更高的可見性,否則應將所有的域都聲明爲私有域”是一個良好的編程習慣,“除非需要某個域是可變的,否則應將其聲明爲final域”也是一個良好的編程習慣。
每當需要對一組相關數據以原子方式執行某個操作時,就可以考慮創建一個不可變的類來包含這些數據。
對數值及其因數分解結果進行緩存的不可變容器類:
/**
* OneValueCache
* <p/>
* Immutable holder for caching a number and its factors
*/
@Immutable
public class OneValueCache {
private final BigInteger lastNumber;
private final BigInteger[] lastFactors;
public OneValueCache(BigInteger i,
BigInteger[] factors) {
lastNumber = i;
lastFactors = Arrays.copyOf(factors, factors.length);
}
public BigInteger[] getFactors(BigInteger i) {
if (lastNumber == null || !lastNumber.equals(i))
return null;
else
return Arrays.copyOf(lastFactors, lastFactors.length);
}
}
使用指向不可變容器對象的volatile類型引用以緩存最新的結果:
/**
* VolatileCachedFactorizer
* <p/>
* Caching the last result using a volatile reference to an immutable holder object
* 當一個線程將volatile類型的cache設置爲引用一個新的OneValueCache時,其他線程就會立即看到新緩存的數據
*/
@ThreadSafe
public class VolatileCachedFactorizer extends GenericServlet implements Servlet {
private volatile OneValueCache cache = new OneValueCache(null, null);
public void service(ServletRequest req, ServletResponse resp) {
BigInteger i = extractFromRequest(req);
BigInteger[] factors = cache.getFactors(i);
if (factors == null) {
factors = factor(i);
cache = new OneValueCache(i, factors);
}
encodeIntoResponse(resp, factors);
}
}
3.5 安全發佈
/**
* Holder
* <p/>
* Class at risk of failure if not properly published
*/
public class Holder {
private int n;
public Holder(int n) {
this.n = n;
}
public void assertSanity() {
if (n != n)
throw new AssertionError("This statement is false.");
}
}
由於沒有使用同步來確保Holder對象對其他線程可見,因此將Holder稱爲“未被正確發佈”。在未被正確發佈的對象中存在兩個問題。首先,除了發佈對象的線程外,其他線程可以看到的Holder域是一個失效值,因此將看到一個空引用或者之前的舊值。然而,更糟糕的情況是,線程看到Holder引用的值是最新的,但Holder狀態的值卻是失效的。情況變得更加不可預測的是,某個線程在第一次讀取域時得到失效值,而再次讀取這個域時會得到一個更新值,這也是assertSanity拋出AssertionError的原因。
任何線程都可以在不需要額外同步的情況下安全地訪問不可變對象,即使在發佈這些對象時沒有使用同步。
要安全地發佈一個對象,對象的引用以及對象的狀態必須同時對其他線程可見。一個正確構造的對象可以通過以下方式來安全地發佈:
- 在靜態初始化函數中初始化一個對象引用。
- 將對象的引用保存到volatile類型的域或者AutomicReference對象中。
- 將對象的引用保存到某個正確構造對象的final類型域中。
- 將對象的引用保存到一個由鎖保護的域中。
通常,要發佈一個靜態構造的對象,最簡單和最安全的方式是使用靜態的初始化器:
public static Holder holder = new Holder(42);
靜態初始化器由JVM在類的初始化階段執行。由於在JVM內部存在着同步機制,因此通過這種方式初始化的任何對象都可以被安全地發佈。
如果對象從技術上來看是可變的,但其狀態在發佈後不會再改變,那麼把這種對象稱爲“事實不可變對象”。通過使用事實不可變對象,不僅可以簡化開發過程,而且還能由於減少了同步而提高性能。在沒有額外的同步的情況下,任何線程都可以安全地使用被安全發佈的事實不可變對象。
對於可變對象,不僅在發佈對象時需要使用同步,而且在每次對象訪問時同樣需要使用同步來確保後續修改操作的可見性。
對象的發佈需求取決於它的可見性:
- 不可變對象可以通過任意機制來發布。
- 事實不可變對象必須通過安全方式來發布。
- 可變對象必須通過安全方式來發布,並且必須是線程安全的或者由某個鎖保護起來。
在併發程序中使用和共享對象時,可以使用一些實用的策略,包括:
- 線程封閉。線程封閉的對象只能由一個線程擁有,對象被封閉在該線程中,並且只能由這個線程修改。
- 只讀共享。在沒有額外同步的情況下,共享的只讀對象可以由多個線程併發訪問,但任何線程都不能修改它。共享的只讀對象包括不可變對象和事實不可變對象。
- 線程安全共享。線程安全的對象在其內部實現同步,因此多個線程可以通過對象的公有接口來進行訪問而不需要進一步的同步。
- 保護對象。被保護的對象只能通過持有特定的鎖來訪問。保護對象包括封裝在其他線程安全對象中的對象,以及已發佈的並且由某個特定鎖保護的對象。
第四章:對象的組合
4.1 設計線程安全的類
通過使用封裝技術,可以使得在不對整個程序進行分析的情況下就可以判斷一個類是否是線程安全的。
在設計一個線程安全類的過程中,需要包含以下三個基本要素:
- 找出構成對象狀態的所有變量。
- 找出約束狀態變量的不變性條件。
- 建立對象狀態的併發訪問管理策略。
4.2 實例封閉
將數據封裝在對象內部,可以將數據的訪問限制在對象的方法上,從而更容易確保線程在訪問數據時總能持有正確的鎖。
封閉機制更易於構造線程安全的類,因爲當封閉類的狀態時,在分析類的線程安全性時就無需檢查整個程序。
4.3 線程安全性的委託
如果一個類是由多個獨立且線程安全的狀態變量組成,並且在所有的操作中都不包含無效狀態轉換,那麼可以將線程安全性委託給底層的狀態變量。
/**
* VisualComponent
* <p/>
* Delegating thread safety to multiple underlying state variables
*/
public class VisualComponent {
private final List<KeyListener> keyListeners
= new CopyOnWriteArrayList<KeyListener>();
private final List<MouseListener> mouseListeners
= new CopyOnWriteArrayList<MouseListener>();
public void addKeyListener(KeyListener listener) {
keyListeners.add(listener);
}
public void addMouseListener(MouseListener listener) {
mouseListeners.add(listener);
}
public void removeKeyListener(KeyListener listener) {
keyListeners.remove(listener);
}
public void removeMouseListener(MouseListener listener) {
mouseListeners.remove(listener);
}
}
/**
* NumberRange
* <p/>
* Number range class that does not sufficiently protect its invariants
*/
public class NumberRange {
// INVARIANT: lower <= upper
private final AtomicInteger lower = new AtomicInteger(0);
private final AtomicInteger upper = new AtomicInteger(0);
public void setLower(int i) {
// Warning -- unsafe check-then-act
if (i > upper.get())
throw new IllegalArgumentException("can't set lower to " + i + " > upper");
lower.set(i);
}
public void setUpper(int i) {
// Warning -- unsafe check-then-act
if (i < lower.get())
throw new IllegalArgumentException("can't set upper to " + i + " < lower");
upper.set(i);
}
public boolean isInRange(int i) {
return (i >= lower.get() && i <= upper.get());
}
}
當把線程安全性委託給某個對象的底層狀態變量時,在什麼條件下纔可以發佈這些變量從而使其他類能修改它們?答案取決於在類中對這些變量施加了哪些不變性條件。如果一個狀態變量是線程安全的,並且沒有任何不變性條件來約束它的值,在變量的操作上也不存在任何不允許的狀態轉換,那麼就可以安全地發佈這個變量。
/**
* SafePoint
*/
@ThreadSafe
public class SafePoint {
@GuardedBy("this") private int x, y;
private SafePoint(int[] a) {
this(a[0], a[1]);
}
public SafePoint(SafePoint p) {
this(p.get());
}
public SafePoint(int x, int y) {
this.set(x, y);
}
public synchronized int[] get() {
return new int[]{x, y};
}
public synchronized void set(int x, int y) {
this.x = x;
this.y = y;
}
}
/**
* PublishingVehicleTracker
* <p/>
* Vehicle tracker that safely publishes underlying state
*/
@ThreadSafe
public class PublishingVehicleTracker {
private final Map<String, SafePoint> locations;
private final Map<String, SafePoint> unmodifiableMap;
public PublishingVehicleTracker(Map<String, SafePoint> locations) {
this.locations = new ConcurrentHashMap<String, SafePoint>(locations);
this.unmodifiableMap = Collections.unmodifiableMap(this.locations);
}
public Map<String, SafePoint> getLocations() {
return unmodifiableMap;
}
public SafePoint getLocation(String id) {
return locations.get(id);
}
public void setLocation(String id, int x, int y) {
if (!locations.containsKey(id))
throw new IllegalArgumentException("invalid vehicle name: " + id);
locations.getLocation(id).set(x, y);
}
}
4.4 在現有的線程安全類中添加功能
- 在原始類中添加一個方法。
- 對類進行擴展。
- 擴展類的功能,但並不是擴展類本身,而是將擴展代碼放入一個“輔助類”中。
- 組合。
/**
* BetterVector
* <p/>
* Extending Vector to have a put-if-absent method
* 對類進行擴展
*/
@ThreadSafe
public class BetterVector <E> extends Vector<E> {
// When extending a serializable class, you should redefine serialVersionUID
static final long serialVersionUID = -3963416950630760754L;
public synchronized boolean putIfAbsent(E x) {
boolean absent = !contains(x);
if (absent)
add(x);
return absent;
}
}
/**
* ListHelper
* <p/>
* Examples of thread-safe and non-thread-safe implementations of
* put-if-absent helper methods for List
* 將擴展代碼放入一個“輔助類”中
*/
@NotThreadSafe
class BadListHelper <E> {
public List<E> list = Collections.synchronizedList(new ArrayList<E>());
public synchronized boolean putIfAbsent(E x) {
boolean absent = !list.contains(x);
if (absent)
list.add(x);
return absent;
}
}//synchronized是輔助類的鎖,在錯誤的鎖上進行了同步
@ThreadSafe
class GoodListHelper <E> {
public List<E> list = Collections.synchronizedList(new ArrayList<E>());
public boolean putIfAbsent(E x) {
synchronized (list) {
boolean absent = !list.contains(x);
if (absent)
list.add(x);
return absent;
}
}
}
/**
* ImprovedList
*
* Implementing put-if-absent using composition
*
*/
@ThreadSafe
public class ImprovedList<T> implements List<T> {
private final List<T> list;
/**
* PRE: list argument is thread-safe.
*/
public ImprovedList(List<T> list) { this.list = list; }
public synchronized boolean putIfAbsent(T x) {
boolean contains = list.contains(x);
if (contains)
list.add(x);
return !contains;
}
// Plain vanilla delegation for List methods.
// Mutative methods must be synchronized to ensure atomicity of putIfAbsent.
public int size() {
return list.size();
}
public boolean isEmpty() {
return list.isEmpty();
}
public boolean contains(Object o) {
return list.contains(o);
}
public Iterator<T> iterator() {
return list.iterator();
}
public Object[] toArray() {
return list.toArray();
}
public <T> T[] toArray(T[] a) {
return list.toArray(a);
}
public synchronized boolean add(T e) {
return list.add(e);
}
public synchronized boolean remove(Object o) {
return list.remove(o);
}
public boolean containsAll(Collection<?> c) {
return list.containsAll(c);
}
public synchronized boolean addAll(Collection<? extends T> c) {
return list.addAll(c);
}
public synchronized boolean addAll(int index, Collection<? extends T> c) {
return list.addAll(index, c);
}
public synchronized boolean removeAll(Collection<?> c) {
return list.removeAll(c);
}
public synchronized boolean retainAll(Collection<?> c) {
return list.retainAll(c);
}
public boolean equals(Object o) {
return list.equals(o);
}
public int hashCode() {
return list.hashCode();
}
public T get(int index) {
return list.get(index);
}
public T set(int index, T element) {
return list.set(index, element);
}
public void add(int index, T element) {
list.add(index, element);
}
public T remove(int index) {
return list.remove(index);
}
public int indexOf(Object o) {
return list.indexOf(o);
}
public int lastIndexOf(Object o) {
return list.lastIndexOf(o);
}
public ListIterator<T> listIterator() {
return list.listIterator();
}
public ListIterator<T> listIterator(int index) {
return list.listIterator(index);
}
public List<T> subList(int fromIndex, int toIndex) {
return list.subList(fromIndex, toIndex);
}
public synchronized void clear() { list.clear(); }
}
4.5 將同步策略文檔化
在文檔中說明客戶代碼需要了解的線程安全性保證,以及代碼維護人員需要了解的同步策略。
第五章:基礎構建模塊
委託是創建線程安全類的一個最有效的策略:只需讓現有的線程安全類管理所有的狀態即可。
5.1 同步容器類
同步容器類包括Vector和Hashtable,還包括一些由Collections.syschronizedXxx等工廠方法創建的同步的封裝器類。這些類實現線程安全的方式是:將它們的狀態封裝起來,並對每個公有方法都進行同步,使得每次只有一個線程能訪問容器的狀態。
同步容器類都是線程安全的,但在某些情況下可能需要額外的客戶端加鎖來保護複合操作。容器上常見的複合操作包括:迭代(反覆訪問元素,直到遍歷完容器中所有元素)、跳轉(根據指定順序找到當前元素的下一個元素)以及條件運算(例如“若沒有則添加”)。
//Vector上可能導致混亂結果的複合操作
public static Object getLast(Vector list){
int lastIndex = list.size() - 1;
return list.get(lastIndex);
}
public static void deleteLast(Vector list){
int lastIndex = list.size() - 1;
list.remove(lastIndex);
}
//在使用客戶端加鎖的Vector上的複合操作
public static Object getLast(Vector list){
syschronized(list){
int lastIndex = list.size() - 1;
return list.get(lastIndex);
}
}
public static void deleteLast(Vector list){
syschronized(list){
int lastIndex = list.size() - 1;
list.remove(lastIndex);
}
}
在設計同步容器類的迭代器時並沒有考慮到併發修改的問題,並且它們表現出的行爲是“及時失敗”(fail-fast)的。這意味着,當它們發現容器在迭代過程中被修改時,就會拋出一個ConcurrentModificationException異常。
這種“及時失敗”的迭代器並不是一種完備的處理機制,而只是“善意地”捕獲併發錯誤,因此只能作爲併發問題的預警指示器。它們採用的實現方式是,將計數器的變化與容器關聯起來:如果在迭代期間計數器被修改,那麼hasNext或next將拋出ConcurrentModificationException。然而,這種檢查是在沒有同步的情況下進行的,因此可能會看到失效的計數器,而迭代器可能並沒有意識到已經發生了修改。這是一種設計上的權衡,從而降低併發修改操作的檢測代碼對程序性能帶來的影響。(在單線程代碼中也可能拋出ConcurrentModificationException異常,當對象直接從容器中刪除而不是通過Iterator.remove來刪除時)
如果不希望在迭代期間對容器加鎖,那麼一種替代方法就是“克隆”容器(在克隆過程中仍然需要對容器加鎖),並在副本上進行迭代。在克隆容器時存在顯著的性能開銷。這種方式的好壞取決於多個因素,包括容器的大小,在每個元素上執行的工作,迭代操作相對於容器其他操作的調用頻率,以及在響應時間和吞吐量等方面的需求。
正如封裝對象的狀態有助於維持不變性條件一樣,封裝對象的同步機制同樣有助於確保實施同步策略。
容器的hashCode和equals等方法也會間接地執行迭代操作,當容器作爲另一個容器的元素或鍵值時,就會出現這種情況。同樣,containsAll、removeAll和retainAll等方法,以及把容器作爲參數的構造函數,都會對容器進行迭代。所有這些間接的迭代操作都可能拋出ConcurrentModificationException。
5.2 併發容器
同步容器將所有對容器狀態的訪問都串行化,以實現它們的線程安全性,這種方法的代價是嚴重降低併發性。併發容器則是針對多個線程併發訪問設計的。
通過併發容器來代替同步容器,可以極大地提高伸縮性並降低風險。
ConcurrentHashMap:
ConcurrentHashMap與其他併發容器一起增強了同步容器類:它們提供的迭代器不會拋出ConcurrentModificationException,因此不需要在迭代過程中對容器加鎖。ConcurrentHashMap返回的迭代器具有弱一致性,而並非“及時失敗”。弱一致性的迭代器可以容忍併發的修改,當創建迭代器時會遍歷已有的元素,並可以(但是不保證)在迭代器被構造後將修改操作反映給容器。
儘管有這些改進,但仍然有一些需要權衡的因素。對於一些需要在整個Map上進行計算的方法,例如size和isEmpty,這些方法的語義被略微減弱了以反映容器的併發特性(例如允許size返回一個近似值而不是一個精確值)。size和isEmpty這樣的方法在併發環境下的用處很小,因爲它們的返回值總在不斷變化。因此,這些操作的需求被弱化了,以換取對其他更重要操作的性能優化,包括put、get、containsKey和remove等。
與Hashtable和syschronizedMap相比,ConcurrentHashMap有着更多的優勢以及更少的劣勢,因此在大多數情況下,用ConcurrentHashMap來代替同步Map能進一步提高代碼的可伸縮性。只有當應用程序需要加鎖Map以進行獨佔訪問(或者需要依賴於同步Map帶來的一些其他作用)時,才應該放棄使用ConcurrentHashMap。
由於ConcurrentHashMap不能被加鎖來執行獨佔訪問,因此我們無法使用客戶端加鎖來創建新的原子操作。但是,一些常見的複合操作,例如“若沒有則添加”、“若相等則移除”和“若相等則替換”等,都已經實現爲原子操作並且在ConcurrentMap接口中聲明。如果你需要在現有的同步Map中添加這樣的功能,那麼很可能就意味着應該考慮使用ConcurrentMap了。
//ConcurrentMap接口
public interface ConcurrentMap<K,V> extends Map<K,V>{
//僅當K沒有相應的映射值時才插入
V putIfAbsent(K key, V value);
//僅當K被映射到V時才移除
boolean remove(K key, V value);
//僅當K被映射到oldValue時才替換爲newValue
boolean replace(K key, V oldValue, V newValue);
//僅當K被映射到某個值時才替換爲newValue
V replace(K key, V newValue);
}
CopyOnWriteArrayList:
CopyOnWriteArrayList用來替代同步List,在某些情況下它提供了更好的併發性能,並且在迭代期間不需要對容器進行加鎖或複製。類似地,CopyOnWriteArraySet的作用是替代同步Set。
“寫入時複製”容器的線程安全性在於,只要正確地發佈一個事實不可變的對象,那麼在訪問該對象時就不再需要進一步的同步。在每次修改時,都會創建並重新發佈一個新的容器副本,從而實現可變性。
顯然,每當修改容器時都會複製底層數組,這需要一定的開銷,特別是當容器的規模較大時。僅當迭代操作遠遠多於修改操作時,才應該使用“寫入時複製”容器。
5.3 阻塞隊列和生產者-消費者模式
阻塞隊列提供了可阻塞的put和take方法,以及支持定時的offer和poll方法。如果隊列已經滿了,那麼put方法將阻塞直到有空間可用;如果隊列爲空,那麼take方法將會阻塞直到有元素可用。隊列可以是有界的也可以是無界的,無界隊列永遠都不會充滿。
在基於阻塞隊列構建的生產者-消費者設計中,當數據生成時,生產者將數據放入隊列,而當消費者準備處理數據時,將從隊列中獲取數據。BlockingQueue簡化了生產者-消費者設計的實現過程,它支持任意數量的生產者和消費者。
阻塞隊列同樣提供了一個offer方法,如果數據項不能被添加到隊列中,那麼將返回一個失敗狀態。這樣你就能夠創建更多靈活的策略來處理負荷過載的情況。
在構建高可靠的應用程序時,有界隊列是一種強大的資源管理工具:它們能抑制並防止產生過多的工作項,使應用程序在負荷過載的情況下變得更加健壯。
在類庫中包含了BlockingQueue的多種實現。其中,LinkedBlockingQueue和ArrayBlockingQueue是FIFO隊列;PriorityBlockingQueue是一個按優先級排序的隊列,既可以根據元素的自然順序來比較元素(如果它們實現了Comparable方法),也可以使用Comparator來比較;最後一個BlockingQueue實現是SyschronousQueue,實際上它不是一個真正的隊列,因爲它不會爲隊列中元素維護存儲空間,它維護一組線程,這些線程在等待着把元素加入或移出隊列。這種實現隊列的方式可以直接交付工作。僅當有足夠多的消費者,並且總是有一個消費者準備好獲取交付的工作時,才適合使用同步隊列。
Java 6 增加了兩種容器類型,Deque 和 BlockingDeque ,它們分別對Queue和BlockingQueue進行了擴展。Deque是一個雙端隊列,實現了在隊列頭和隊列尾的高效插入和移除。具體實現包括ArrayDeque和LinkedBlockingQueue。正如阻塞隊列適用於生產者-消費者模式,雙端隊列同樣適用於另一種相關模式,即工作密取。在生產者-消費者設計中,所以消費者有一個共享的工作隊列,而在工作密取設計中,每個消費者都有各自的雙端隊列(極大地減少了競爭)。如果一個消費者完成了自己雙端隊列中的全部工作,那麼它可以從其他消費者雙端隊列的末尾(從尾部而不是頭部獲取工作,進一步降低了隊列上的競爭程度)祕密地獲取工作。工作密取非常適用於既是消費者也是生產者問題—當執行某個工作時可能導致出現更多的工作。例如,在網頁爬蟲程序中處理一個頁面時,通常會發現有更多的頁面需要處理。當一個工作線程找到新的任務單元時,它會將其放到自己隊列的末尾(或者在工作共享設計模式中,放入其他工作者線程的隊列中)。當雙端隊列爲空時,它會在另一個線程的隊列隊尾查找新的任務,從而確保每個線程都保持忙碌狀態。
5.4 阻塞方法與中斷方法
線程阻塞或暫停的原因:等待I/O操作結束,等待獲得一個鎖,等待從Thread.sleep方法中醒來,或是等待另一個線程的計算結果。
當某方法拋出InterruptedException時,表示該方法是一個阻塞方法,如果這個方法被中斷,那麼它將努力提前結束阻塞狀態。
Thread提供了interrupt方法,用於中斷線程或者查詢線程是否已經被中斷。每個線程都有一個布爾類型的屬性,表示線程的中斷狀態,當中斷線程時將設置這個狀態。
中斷是一種協作機制。一個線程不能強制其他線程停止正在執行的操作而去執行其他的操作。當線程A中斷B時,A僅僅是要求B在執行到某個可以暫停的地方停止正在執行的操作——前提是如果線程B願意停止下來。最常使用中斷的情況就是取消某個操作。方法對中斷請求的響應度越高,就越容易及時取消那些執行時間很長的操作。
處理對中斷的響應:
- 傳遞InterruptedException。避開這個異常通常是最明智的策略——只需把InterruptedException傳遞給方法的調用者。
- 恢復中斷。有時候不能拋出InterruptedException,例如當代碼是Runnable的一部分時。在這些情況下必須捕獲InterruptedException,並通過調用當前線程上的interrupt方法恢復中斷狀態,這樣在調用棧中更高層的代碼將看到引發了一箇中斷。
/**
* TaskRunnable
* <p/>
* Restoring the interrupted status so as not to swallow the interrupt
*/
public class TaskRunnable implements Runnable {
BlockingQueue<Task> queue;
public void run() {
try {
processTask(queue.take());
} catch (InterruptedException e) {
// restore interrupted status
Thread.currentThread().interrupt();
}
}
void processTask(Task task) {
// Handle the task
}
interface Task {
}
}
在出現InterruptedException時不應該做的事情是,捕獲它但不做出任何響應。只有在一種特殊的情況下才能屏蔽中段,即對Thread進行擴展,並且能控制調用棧上所有更高層的代碼。
5.5 同步工具類
同步工具類:阻塞隊列、信號量(Semaphore)、柵欄(Barrier)以及閉鎖(Latch)等。
所有的同步工具類都包含一些特定的結構化屬性:它們封裝了一些狀態,這些狀態將決定執行同步工具類的線程是繼續執行還是等待,此外還提供了一些方法對狀態進行操作,以及另一些方法用於高效地等待同步工具類進入到預期狀態。
閉鎖:
閉鎖的作用相當於一扇門:在閉鎖到達結束狀態之前,這扇門一直是關閉的,並且沒有任何線程能通過,當到達結束狀態時,這扇門會打開並允許所有的線程通過。當閉鎖到達結束狀態後,將不會再改變狀態,因此這扇門將永遠保持打開狀態。閉鎖可以用來確保某些活動直到其他活動都完成後才繼續執行。
1.CountDownLatch:
一種靈活的閉鎖實現,閉鎖狀態包括一個計數器,該計數器被初始化爲一個正數,表示需要等待的事件數量。countDown方法遞減計數器,表示有一個事件已經發生了,而await方法等待計數器達到零,這表示所有需要等待的事件都已經發生。如果計數器的值非零,那麼await會一直阻塞直到計數器達到零,或者等待中的線程中斷,或者等待超時。
/**
* TestHarness
* <p/>
* Using CountDownLatch for starting and stopping threads in timing tests
*/
public class TestHarness {
public long timeTasks(int nThreads, final Runnable task)
throws InterruptedException {
final CountDownLatch startGate = new CountDownLatch(1);
final CountDownLatch endGate = new CountDownLatch(nThreads);
for (int i = 0; i < nThreads; i++) {
Thread t = new Thread() {
public void run() {
try {
startGate.await();
try {
task.run();
} finally {
endGate.countDown();
}
} catch (InterruptedException ignored) {
}
}
};
t.start();
}
long start = System.nanoTime();
startGate.countDown();
endGate.await();
long end = System.nanoTime();
return end - start;
}
}
啓動門將使得主線程能夠同時釋放所有工作線程,而結束門則使主線程能夠等待最後一個線程執行完成。
2.FutureTask:
FutureTask實現了Future語義,表示一種抽象的可生成結果的計算。FutureTask表示的計算是通過Callable來實現的,相當於一種可生成結果的Runnable,並且可以處於以下三種狀態:等待運行、正在運行和運行完成。“執行完成”表示計算的所有可能結束方式,包括正常結束、由於取消而結束和由於異常而結束等。當FutureTask進入完成狀態後,它會永遠停止在這個狀態上。
Future.get的行爲取決於任務的狀態。如果任務已經完成,那麼get會立即返回結果,否則get將阻塞直到任務進入完成狀態,然後返回結果或者拋出異常。FutureTask將計算結果從執行計算的線程傳遞到獲取這個結果的線程,而FutureTask的規範確保了這種傳遞過程能實現結果的安全發佈。
FutureTask在Executor框架中表示異步任務,此外還可以表示一些時間較長的計算,這些計算可以在使用計算結果之前啓動。通過提前啓動計算,可以減少在等待結果時需要的時間。
/**
* Preloader
*
* Using FutureTask to preload data that is needed later
*/
public class Preloader {
ProductInfo loadProductInfo() throws DataLoadException {
return null;
}
private final FutureTask<ProductInfo> future =
new FutureTask<ProductInfo>(new Callable<ProductInfo>() {
public ProductInfo call() throws DataLoadException {
return loadProductInfo();
}
});
private final Thread thread = new Thread(future);
public void start() { thread.start(); }
public ProductInfo get()
throws DataLoadException, InterruptedException {
try {
return future.get();
} catch (ExecutionException e) {
Throwable cause = e.getCause();
if (cause instanceof DataLoadException)
throw (DataLoadException) cause;
else
throw LaunderThrowable.launderThrowable(cause);
}
}
interface ProductInfo {
}
}
class DataLoadException extends Exception { }
/**
* StaticUtilities
*
*/
public class LaunderThrowable {
/**
* Coerce an unchecked Throwable to a RuntimeException
* <p/>
* If the Throwable is an Error, throw it; if it is a
* RuntimeException return it, otherwise throw IllegalStateException
*/
public static RuntimeException launderThrowable(Throwable t) {
if (t instanceof RuntimeException)
return (RuntimeException) t;
else if (t instanceof Error)
throw (Error) t;
else
throw new IllegalStateException("Not unchecked", t);
}
}
信號量:
計數信號量用來控制同時訪問某個特定資源的操作數量,或者同時執行某個指定操作的數量。計數信號量還可以用來實現某種資源池,或者對容器施加邊界。
Semaphore中管理着一組虛擬的許可,許可的初始數量可通過構造函數來指定。在執行操作時首先獲得許可(只要還有剩餘的許可),並在使用以後釋放許可。如果沒有許可,那麼acquire將阻塞直到有許可(或者直到被中斷或者操作超時)。release方法將返回一個許可給信號量。在一個線程中獲得的許可可以在另一個線程中釋放。可以將acquire操作視爲消費一個許可,release操作創建一個許可。Semaphore並不受限於它在創建時的初始許可數量。
計算信號量的一種簡化形式是二值信號量,即初始值爲1的Semaphore。二值信號量可以用作互斥體(mutex),並具備不可重入的加鎖語義:誰擁有這個唯一的許可,誰就擁有了互斥鎖。
/**
* BoundedHashSet
* <p/>
* Using Semaphore to bound a collection
* 使用Semaphore爲容器設置邊界
*/
public class BoundedHashSet <T> {
private final Set<T> set;
private final Semaphore sem;
public BoundedHashSet(int bound) {
this.set = Collections.synchronizedSet(new HashSet<T>());
sem = new Semaphore(bound);
}
public boolean add(T o) throws InterruptedException {
sem.acquire();//添加元素前獲取許可
boolean wasAdded = false;
try {
wasAdded = set.add(o);
return wasAdded;
} finally {
if (!wasAdded)
sem.release();//add操作不成功則釋放許可
}
}
public boolean remove(Object o) {
boolean wasRemoved = set.remove(o);
if (wasRemoved)
sem.release();
return wasRemoved;
}
}
柵欄:
柵欄類似於閉鎖,它能阻塞一組線程直到某個事件發生。柵欄與閉鎖的關鍵區別在於,所有線程必須同時到達柵欄位置,才能繼續執行。閉鎖用於等待事件,而柵欄用於等待其他線程。
1.CyclicBarrier
可以使一定數量的參與方反覆地在柵欄位置彙集,它在並行迭代算法中非常有用:這種算法通常將一個問題拆分爲一系列相互獨立的子問題。當線程到達柵欄位置時將調用await方法,這個方法將阻塞直到所有線程都到達柵欄位置。如果所有線程都到達了柵欄位置,那麼柵欄將打開,此時所有線程都被釋放,而柵欄將被重置以便下次使用。如果對await的調用超時,或者await的阻塞線程被中斷,那麼柵欄就被認爲是打破了,所有阻塞的await調用都將終止並拋出BrokenBarrierException。如果成功地通過柵欄,那麼await將爲每個線程返回一個唯一的到達索引號,我們可以利用這些索引來”選舉“產生一個領導線程,並在下一次迭代中由該領導線程執行一些特殊的工作。CyclicBarrier還可以使你將一個柵欄操作傳遞給構造函數,這是一個Runnable,當成功通過柵欄時會(在一個子任務線程中)執行它,但在阻塞線程被釋放之前是不能被執行的。
2.Exchanger
一種兩方柵欄,各方在柵欄位置上交換數據。當兩方執行不對稱的操作時,Exchanger會非常有用,例如當一個線程向緩衝區寫入數據,而另一個線程從緩衝區中讀取數據。
5.6 構建高效且可伸縮的結果緩存
interface Computable <A, V> {
V compute(A arg) throws InterruptedException;
}
public class Memoizer <A, V> implements Computable<A, V> {
private final ConcurrentMap<A, Future<V>> cache
= new ConcurrentHashMap<A, Future<V>>();
private final Computable<A, V> c;
public Memoizer(Computable<A, V> c) {
this.c = c;
}
public V compute(final A arg) throws InterruptedException {
while (true) {
Future<V> f = cache.get(arg);
if (f == null) {
Callable<V> eval = new Callable<V>() {
public V call() throws InterruptedException {
return c.compute(arg);
}
};
FutureTask<V> ft = new FutureTask<V>(eval);
f = cache.putIfAbsent(arg, ft);
if (f == null) {
f = ft;
ft.run();
}
}
try {
return f.get();
} catch (CancellationException e) {
cache.remove(arg, f);
} catch (ExecutionException e) {
throw LaunderThrowable.launderThrowable(e.getCause());
}
}
}
}
仍然存在一些問題:緩存污染(緩存的是Future而不是值,可能計算被取消或失敗);緩存逾期;緩存清理。
/**
* Factorizer
* <p/>
* Factorizing servlet that caches results using Memoizer
*/
@ThreadSafe
public class Factorizer extends GenericServlet implements Servlet {
private final Computable<BigInteger, BigInteger[]> c =
new Computable<BigInteger, BigInteger[]>() {
public BigInteger[] compute(BigInteger arg) {
return factor(arg);
}
};
private final Computable<BigInteger, BigInteger[]> cache
= new Memoizer<BigInteger, BigInteger[]>(c);
public void service(ServletRequest req,
ServletResponse resp) {
try {
BigInteger i = extractFromRequest(req);
encodeIntoResponse(resp, cache.compute(i));
} catch (InterruptedException e) {
encodeError(resp, "factorization interrupted");
}
}
void encodeIntoResponse(ServletResponse resp, BigInteger[] factors) {
}
void encodeError(ServletResponse resp, String errorString) {
}
BigInteger extractFromRequest(ServletRequest req) {
return new BigInteger("7");
}
BigInteger[] factor(BigInteger i) {
// Doesn't really factor
return new BigInteger[]{i};
}
}
第一部分小結
- 可變狀態是至關重要的。
所有的併發問題都可以歸結爲如何協調對併發狀態的訪問。可變狀態越少,就越容易確保線程安全性。 - 儘量將域聲明爲final類型,除非需要它們是可變的。
- 不可變對象一定是線程安全的。
不可變對象能極大地降低併發編程的複雜性。它們更爲簡單而且安全,可以任意共享而無須使用加鎖或保護性複製等機制。 - 封裝有助於管理複雜性。
在編寫線程安全的程序時,雖然可以將所有數據都保存在全局變量中,但爲什麼要這麼做?將數據封裝在對象中,更易於維持不變性條件;將同步機制封裝在對象中,更易於遵循同步策略。 - 用鎖來保護每個可變變量。
- 當保護同一個不變性條件中的所有變量時,要使用同一個鎖。
- 在執行復合操作期間,要持有鎖。
- 如果從多個線程中訪問同一個可變變量時沒有同步機制,那麼程序會出現問題。
- 不要故作聰明地推斷出不需要使用同步。
- 在設計過程中考慮線程安全,或者在文檔中明確地指出它不是線程安全的。
- 將同步策略文檔化。
第二部分:結構化併發應用程序
第六章:任務執行
6.1 在線程中執行任務
大多數服務器應用程序都提供了一種自然的任務邊界選擇方式:以獨立的客戶請求爲邊界。
任務執行策略:
1.串行地執行任務:通常無法提供高吞吐率或快速響應性。
2.爲每個請求創建一個新的線程:線程生命週期的開銷非常高;資源消耗;穩定性。在一定的範圍內,增加線程可以提高系統的吞吐率,但如果超出了這個範圍,再創建更多的線程只會降低程序的執行速度,並且如果過多地創建線程,那麼整個應用程序將崩潰。
6.2 Executor框架
//Executor接口
public interface Executor {
void execute(Runnable command);
}
該框架能支持多種不同類型的任務執行策略。它提供了一種標準的方法將任務的提交過程與執行過程解耦開來,並用Runnable來表示任務。
Executor基於生產者-消費者模式,提交任務的操作相當於生產者,執行任務的線程則相當於消費者。
/**
* TaskExecutionWebServer
* <p/>
* Web server using a thread pool
*/
public class TaskExecutionWebServer {
private static final int NTHREADS = 100;
private static final Executor exec
= Executors.newFixedThreadPool(NTHREADS);
public static void main(String[] args) throws IOException {
ServerSocket socket = new ServerSocket(80);
while (true) {
final Socket connection = socket.accept();
Runnable task = new Runnable() {
public void run() {
handleRequest(connection);
}
};
exec.execute(task);
}
}
private static void handleRequest(Socket connection) {
// request-handling logic here
}
}
/**
* ThreadPerTaskExecutor
* <p/>
* Executor that starts a new thread for each task
*/
public class ThreadPerTaskExecutor implements Executor {
public void execute(Runnable r) {
new Thread(r).start();
};
}
/**
* WithinThreadExecutor
* <p/>
* Executor that executes tasks synchronously in the calling thread
*/
public class WithinThreadExecutor implements Executor {
public void execute(Runnable r) {
r.run();
};
}
各種執行策略都是一種資源管理工具,最佳策略取決於可用的計算資源以及對服務質量的需求。通過將任務的提交與任務的執行策略分離開來,有助於在部署階段選擇與可用硬件資源最匹配的執行策略。
每當看到下面這種形式的代碼時:
new Thread(runnable).start();
並且你希望獲得一種更靈活的執行策略時,請考慮使用Executor來代替Thread。
線程池:
“在線程池中執行任務”比“爲每個任務分配一個線程”優勢更多。通過重用現有的線程而不是創建新線程,可以在處理多個請求時分攤在線程創建和銷燬過程中產生的巨大開銷。另一個額外的好處是,當請求到達時,工作線程通常已經存在,因此不會由於等待創建線程而延遲任務的執行,從而提高了響應性。通過適當調整線程池的大小,可以創建足夠多的線程以便使處理器保持忙碌狀態,同時還可以防止過多線程相互競爭資源而使應用程序耗盡內存或失敗。
可以通過調用Executors中的靜態工廠方法之一來創建一個線程池:
newFixedThreadPool:newFixedThreadPool將創建一個固定長度的線程池,每當提交一個任務時就創建一個線程,直到達到線程池的最大數量,這時線程池的規模將不再發生變化(如果某個線程由於發生了未預期的Exception而結束,那麼線程池將補充一個新的線程)。
newCachedThreadPool:newCachedThreadPool將創建一個可緩存的線程池,如果線程池的當前規模超過了處理需求時,那麼將回收空閒的線程,而當需求增加時,則可以添加新的線程,線程池的規模不存在任何限制。
newSingleThreadExecutor:newSingleThreadExecutor是一個單線程的Executor,它創建單個工作者線程來執行任務,如果這個線程異常結束,會創建另一個線程來替代。newSingleThreadExecutor能確保依照任務在隊列中的順序來串行執行(例如FIFO、LIFO、優先級)。單線程的Executor還提供了大量的內部同步機制,從而確保了任務執行的任何內存寫入操作對於後續任務來說都是可見的。這意味着,即使這個線程會不時地被另一個線程替代,但對象總是可以安全地封閉在“任務線程”中。
newScheduledThreadPool:newScheduledThreadPool創建了一個固定長度的線程池,而且以延遲或定時的方式來執行任務,類似於Timer。
Timer支持基於絕對時間而不是相對時間的調度機制,因此任務的執行對系統時鐘變化很敏感,而ScheduledThreadPoolExecutor只支持基於相對時間的調度。Timer在執行所有定時任務時只會創建一個線程,如果某個任務的執行時間過長,那麼將會破壞其他TimerTask的定時精確性。Timer的另一個問題是,如果TimerTask拋出了一個未檢查的異常,那麼Timer將表現出糟糕的行爲。Timer線程並不捕獲異常,因此當TimerTask拋出未檢查的異常時將終止定時線程。這種情況下,Timer也不會恢復線程的執行,而是會錯誤地認爲整個Timer都被取消了。這個問題稱之爲“線程泄露”。在Java 5.0 或更高的JDK 中,將很少使用Timer。
如果要構建自己的調度服務,那麼可以使用DelayQueue,它實現了BlockingQueue,併爲ScheduledThreadPoolExecutor提供調度功能。DelayQueue管理着一組Delayed對象。每個Delayed對象都有一個相應的延遲時間:在DelayQueue中,只有某個元素逾期後,才能從DelayQueue中執行take操作。從DelayQueue中返回的對象將根據它們的延遲時間進行排序。
Executor執行的任務有4個生命週期階段:創建、提交、開始和完成。由於有些任務可能要執行很長的時間,因此通常希望能夠取消這些任務。在Executor框架中,已提交但尚未開始的任務可以取消,但對於那些已經開始執行的任務,只有當它們能響應中斷時才能取消。取消一個已經完成的任務沒有任何影響。
Future表示一個任務的生命週期,並提供了相應的方法來判斷是否已經完成或取消,以及獲取任務的結果和取消任務等。在Future規範中包含的隱含意義是,任務的生命週期只能前進,不能後退。當某個任務完成後,它就永遠停留在“完成”狀態上。get方法取決於任務的狀態(尚未開始、正在運行、已完成)。如果任務已經完成,那麼get會立即返回或者拋出一個Exception;如果任務沒有完成,那麼get將阻塞並直到任務完成。如果任務拋出了異常,那麼get將該異常封裝爲ExecutionException並重新拋出。如果任務被取消,那麼get將拋出CancellationException。如果get拋出了ExecutionException,那麼可以通過getCause來獲得被封裝的初始異常。
PS:
Callable 和 Runnable 的使用方法大同小異, 區別在於:
1.Callable 使用 call() 方法, Runnable 使用 run() 方法
2.call() 可以返回值, 而 run()方法不能返回。
3.call() 可以拋出受檢查的異常,比如ClassNotFoundException, 而run()不能拋出受檢查的異常。
4.ExecutorService 在Callable中使用的是submit(), 在Runnable中使用的是 execute()。
CompletionService:
將Executor和BlockingQueue的功能融合在一起。你可以將Callable任務提交給它來執行,然後使用類似於隊列操作的take和poll等方法來獲得已完成的結果,而這些結果會在完成時被封裝爲Future。ExecutorCompletionService實現了CompletionService,並將計算部分委託給一個Executor。
/**
* Renderer
* <p/>
* Using CompletionService to render page elements as they become available
*/
public abstract class Renderer {
private final ExecutorService executor;
Renderer(ExecutorService executor) {
this.executor = executor;
}
void renderPage(CharSequence source) {
final List<ImageInfo> info = scanForImageInfo(source);
CompletionService<ImageData> completionService =
new ExecutorCompletionService<ImageData>(executor);
for (final ImageInfo imageInfo : info)
completionService.submit(new Callable<ImageData>() {
public ImageData call() {
return imageInfo.downloadImage();
}
});
renderText(source);
try {
for (int t = 0, n = info.size(); t < n; t++) {
Future<ImageData> f = completionService.take();
ImageData imageData = f.get();
renderImage(imageData);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} catch (ExecutionException e) {
throw launderThrowable(e.getCause());
}
}
interface ImageData {
}
interface ImageInfo {
ImageData downloadImage();
}
abstract void renderText(CharSequence s);
abstract List<ImageInfo> scanForImageInfo(CharSequence s);
abstract void renderImage(ImageData i);
}
多個ExecutorCompletionService可以共享一個Executor,因此可以創建一個對於特定計算私有,又能共享一個Executor的ExecutorCompletionService。
有時候,如果某個任務無法在指定時間內完成, 那麼將不再需要它的結果,此時可以放棄這個任務。在支持時間限制的Future.get中:當結果可用時,它將立即返回;如果在指定時限內沒有計算出結果,那麼將拋出TimeoutException。在使用限時任務時需要注意,當這些任務超時後應該立即停止,從而避免爲繼續計算一個不再使用的結果而浪費計算資源。傳遞給get的timeout參數的計算方法是,將指定時限減去當前時間,這可能會得到負數,但java.util.concurrent中所有與時限相關的方法都將負數視爲零,因此不需要額外的代碼來處理這種情況。
/**
* RenderWithTimeBudget
*
* Fetching an advertisement with a time budget
*/
public class RenderWithTimeBudget {
private static final Ad DEFAULT_AD = new Ad();
private static final long TIME_BUDGET = 1000;
private static final ExecutorService exec = Executors.newCachedThreadPool();
Page renderPageWithAd() throws InterruptedException {
long endNanos = System.nanoTime() + TIME_BUDGET;
Future<Ad> f = exec.submit(new FetchAdTask());
// Render the page while waiting for the ad
Page page = renderPageBody();
Ad ad;
try {
// Only wait for the remaining time budget
long timeLeft = endNanos - System.nanoTime();
ad = f.get(timeLeft, NANOSECONDS);
} catch (ExecutionException e) {
ad = DEFAULT_AD;
} catch (TimeoutException e) {
ad = DEFAULT_AD;
f.cancel(true);
}
page.setAd(ad);
return page;
}
Page renderPageBody() { return new Page(); }
static class Ad {
}
static class Page {
public void setAd(Ad ad) { }
}
static class FetchAdTask implements Callable<Ad> {
public Ad call() {
return new Ad();
}
}
}
invokeAll:支持限時,將多個任務提交到一個ExecutorService並獲得結果。invokeAll方法的參數爲一組任務,並返回一組Future。這兩個集合有着相同的結構。invokeAll按照任務集合中迭代器的順序將所有的Future添加到返回的集合中,從而使調用者能將各個Future與其表示的Callable關聯起來。當所有任務都執行完畢時,或者調用線程被中斷時,又或者超過指定時限時,invokeAll將返回。當超過指定時限後,任何還未完成的任務都會取消。當invokeAll返回後,每個任務要麼正常地完成,要麼被取消,而客戶端代碼可以調用get或isCancelled來判斷究竟是何種情況。
/**
* 引自:http://zld406504302.iteye.com/blog/1840091
*10個班級,每個班級20名學生,在指定的時間內查詢每個班級學生的集合。
*/
public class FutureTest {
//緩存操作數據集
private static final Map<Integer, List<Student>> sutdenMap = new HashMap<Integer, List<Student>>();
//初始化操作數據
static {
List<Student> stuList = null;
Student stu;
for (int i = 0; i < 10; i++) {
stuList = new ArrayList<Student>();
for (int j = 0; j < 20; j++) {
stu = new Student(j, "zld_" + i + "." + j, i);
stuList.add(stu);
}
sutdenMap.put(i, stuList);
}
}
public static class Student {
private int id;
private String name;
private int classID;
public Student(int id, String name, int classID) {
this.id = id;
this.name = name;
this.classID = classID;
}
public String toString() {
return Student.class.getName() + "(id:" + this.id + ",name:"
+ this.name + ")";
}
}
/**
* @filename: SearchTask
* @description: 查詢任務
* @author lida
* @date 2013-4-1 下午3:02:29
*/
public static class SearchTask implements Callable<List<Student>> {
public final int classID;
public long sleepTime;
/**
* <p>Title: </p>
* <p>Description: </p>
* @param classID 班級編號
* @param sleepTime 模擬操作所用的時間數(毫秒)
*/
SearchTask(int classID, long sleepTime) {
this.classID = classID;
this.sleepTime = sleepTime;
}
@Override
public List<Student> call() throws Exception {
//模擬操作所用的時間數(毫秒)
Thread.sleep(sleepTime);
List<Student> stuList = sutdenMap.get(classID);
return stuList;
}
}
public static void main(String[] args) {
FutureTest ft = new FutureTest();
ExecutorService exec = Executors.newCachedThreadPool();
List<SearchTask> searchTasks = new ArrayList<SearchTask>();
SearchTask st;
for (int i = 0; i < 10; i++) {
st = new SearchTask(i, 2001);//指定2000毫秒爲最大執行時間
searchTasks.add(st);
}
try {
//要求在2000毫秒內返回結果,否則取消執行。
List<Future<List<Student>>> futures = exec.invokeAll(searchTasks,
2000, TimeUnit.MILLISECONDS);//invokeAll 第一個參數是任務列表;第二個參數是過期時間;第三個是過期時間單位
for (Future<List<Student>> future : futures) {
List<Student> students = future.get();
for (Student student : students) {
System.out.println(student.toString());
}
}
exec.shutdown();
} catch (InterruptedException e) {
e.printStackTrace();
Thread.interrupted();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}
第七章:取消與關閉
7.1 任務取消
如果外部代碼能在某個操作正常完成之前將其置入“完成”狀態,那麼這個操作就可以稱爲“可取消的”。
取消某個操作的原因:
- 用戶請求取消。
- 有時間限制的操作。
- 應用程序事件。例如,應用程序對某個問題空間進行分解並搜索,從而使不同的任務可以搜索問題空間中的不同區域。當其中一個任務找到了解決方案時,所有其他仍在搜索的任務都將被取消。
- 錯誤。網頁爬蟲程序搜素相關的頁面,並將頁面或摘要數據保存到硬盤。當一個爬蟲任務發生錯誤時(例如,磁盤空間已滿),那麼所有搜索任務都會取消。此時可能會記錄它們的當前狀態,以便稍後稍後重新啓動。
- 關閉。當一個程序或服務關閉時,必須對正在處理和等待處理的工作執行某種操作。在平緩的關閉過程中,當前正在執行的任務將繼續執行直到完成;而在立即關閉過程中,當前的任務則可能取消。
Java中沒有一種安全的搶佔式方式來停止線程,因此也就沒有安全的搶佔式方式來停止任務。只有一些協作式的機制,使請求取消的任務和代碼都遵循一種協商好的協議。
/**
* PrimeGenerator
* <p/>
* Using a volatile field to hold cancellation state
*/
@ThreadSafe
public class PrimeGenerator implements Runnable {
private static ExecutorService exec = Executors.newCachedThreadPool();
@GuardedBy("this") private final List<BigInteger> primes
= new ArrayList<BigInteger>();
private volatile boolean cancelled;
public void run() {
BigInteger p = BigInteger.ONE;
while (!cancelled) {
p = p.nextProbablePrime();
synchronized (this) {
primes.add(p);
}
}
}
public void cancel() {
cancelled = true;
}
public synchronized List<BigInteger> get() {
return new ArrayList<BigInteger>(primes);
}
static List<BigInteger> aSecondOfPrimes() throws InterruptedException {
PrimeGenerator generator = new PrimeGenerator();
exec.execute(generator);
try {
SECONDS.sleep(1);
} finally {
generator.cancel();
}
return generator.get();
}
}
一個可取消的任務必須擁有取消策略:其他代碼如何(How)請求取消該任務,任務在何時(When)檢查是否已經請求了取消,以及在響應取消請求時應該執行哪些(What)操作。
在Java的API或語言規範中,並沒有將中斷與任何取消語義關聯起來,但實際上,如果在取消之外的其他操作中使用中斷,那麼都是不合適的,並且很難支撐起更大的應用。
每個線程都有一個boolean類型的中斷狀態。當中斷線程時,這個線程的中斷狀態將被設置爲true。在Thread中包含了中斷線程以及查詢線程中斷狀態的方法,interrupt方法能中斷目標線程,isInterrupted方法能返回目標線程的中斷狀態,靜態的interrupted方法將清除當前線程的中斷狀態,並返回它之前的值,這也是清除中斷狀態的唯一方法。
public class Thread{
public void interrupt() {...}
public boolean isInterrupted() {...}
public static boolean interrupted() {...}
}
調用interrupt並不意味着立即停止目標線程正在進行的工作,而只是傳遞了請求中斷的消息。
對中斷操作的正確理解是:它並不會真正地中斷一個正在運行的線程,而只是發出中斷請求,然後由線程在下一個合適的時刻中斷自己。(這些時刻也被稱爲取消點)
在使用靜態的interrupted時應該非常小心,因爲它會清除當前線程的中斷狀態。如果在調用interrupted時返回了true,那麼除非你想屏蔽這個中斷,否則必須對它進行處理——可以拋出InterruptedException,或者通過再次調用interrupt來恢復中斷狀態。
通常,中斷是實現取消的最合理方式。
區分任務和線程對中斷的反應是很重要的。任務不會在其自己擁有的線程中執行,而是在某個服務(例如線程池)擁有的線程中執行。對於非線程所有者的代碼來說,應該小心地保存中斷狀態,這樣擁有線程的代碼才能對中斷做出響應,即使“非所有者”代碼也可以做出響應。這就是爲什麼大多數可阻塞的庫函數都只是拋出InterruptedException作爲中斷響應:儘快退出執行流程,並把中斷信息傳遞給調用者,從而使調用棧中的上層代碼可以採取進一步的操作。如果除了將InterruptedException傳遞給調用者外還需要執行其他操作,那麼應該在捕獲InterruptedException之後恢復中斷狀態:
Thread.currentThread().interrupt;
線程應該只能由其所有者中斷,所有者可以將線程的中斷策略信息封裝到某個合適的取消機制中,例如關閉(shutdown)方法。
由於每個線程擁有各自的中斷策略,因此除非你知道中斷對該線程的含義,否則就不應該中斷這個線程。
只有實現了線程中斷策略的代碼纔可以屏蔽中斷請求,在常規的任務和庫代碼中都不應該屏蔽中斷請求。
通常,可中斷的方法會在阻塞或進行重要的工作前首先檢查中斷,從而儘快地響應中斷。
通過Future來實現取消:
ExecutorService.submit將返回一個Future來描述任務。Future擁有一個cancel方法,該方法帶有一個boolean類型的參數mayInterruptIfRunning,表示取消操作是否成功。(這只是表示任務是否能夠接收中斷,而不是表示任務是否能檢測並處理中斷。)如果mayInterruptIfRunning爲true並且任務當前正在某個線程中運行,那麼這個線程能被中斷。如果這個參數爲false,那麼意味着“若任務還沒有啓動,就不要運行它”,這種方式應該用於那些不處理中斷的任務中。
當嘗試取消某個任務時,不宜直接中斷線程池,因爲你並不知道當中斷請求到達時正在運行什麼任務——只能通過任務的Future來實現取消。
//通過Future來取消任務
public static void timedRun(Runnable r, long timeout, TimeUnit unit) throws InterruptedException {
Future<?> task = taskExec.submit(r);
try{
task.get(timeout, unit);
}catch(TimeoutException e) {
//接下來任務將被取消
}catch(ExecutionException e) {
//如果在任務中拋出了異常,那麼重新拋出該異常
throw launderThrowable(e.getCause());
}finally{
//如果任務已經結束,那麼執行取消操作也不會帶來任何影響
task.cancel(true);//如果任務正在進行,那麼將被中斷
}
}
當Future.get拋出InterruptedException或TimeoutException時,如果你知道不再需要結果,那麼就可以調用Future.cancel來取消任務。
正確的封裝原則是:除非擁有某個線程,否則不能對該線程進行操控。線程有一個相應的所有者,即創建該線程的類。
與其他封裝對象一樣,線程的所有權是不可傳遞的:應用程序可以擁有服務,服務也可以擁有工作者線程,但應用程序並不能擁有工作者線程,因此應用程序不能直接停止工作者線程。
對於持有線程的服務,只要服務的存在時間大於創建線程的方法的存在時間,那麼就應該提供生命週期方法。例如,在ExecutorService中提供了shutdown和shutdownNow等方法。
當一個線程由於未捕獲異常而退出時,JVM會把這個事件報告給應用程序提供的UncaughtExceptionHandler異常處理器。如果沒有提供任何異常處理器,那麼默認的行爲是將棧追蹤信息輸出到System.error。
public interface UncaughtExceptionHandler {
void uncaughtException(Thread t, Throwable e);
}
異常處理器如何處理未捕獲異常,取決於對服務質量的需求。最常見的響應方式是將一個錯誤信息以及相應的棧追蹤信息寫入應用程序日誌中。異常處理器還可以採取更直接的響應,例如嘗試重新啓動線程,關閉應用程序,或者執行其他修復或診斷等操作。
/**
* UEHLogger
* <p/>
* UncaughtExceptionHandler that logs the exception
*/
public class UEHLogger implements Thread.UncaughtExceptionHandler {
public void uncaughtException(Thread t, Throwable e) {
Logger logger = Logger.getAnonymousLogger();
logger.log(Level.SEVERE, "Thread terminated with exception: " + t.getName(), e);
}
}
在運行時間較長的應用程序中,通常會爲所有線程的未捕獲異常指定同一個異常處理器,並且該處理器至少會將異常信息記錄到日誌中。
與所有的線程操控一樣,只有線程的所有者能夠改變線程的UncaughtExceptionHandler。
如果你希望在任務由於發生異常而失敗時獲得通知,並且執行一些特定於任務的恢復操作,那麼可以將任務封裝在能捕獲異常的Runnable或Callable中,或者改寫ThreadPoolExecutor的afterExecute方法。
只有通過execute提交的任務,才能將它拋出的異常交給未捕獲異常處理器,而通過submit提交的任務,無論是拋出的未檢查異常還是已檢查異常,都將被認爲是任務返回狀態的一部分。如果一個由submit提交的任務由於拋出了異常而結束,那麼這個異常將被Future.get封裝在ExecutionException中重新拋出。
7.4 JVM關閉
JVM既可以正常關閉,也可以強行關閉。正常關閉的觸發方式包括:當最後一個“正常(非守護)”線程結束時,或者當調用了System.exit時,或者通過其它特定於平臺的方法關閉時(例如發送了SIGINT信號或鍵入Ctrl-C)。強行關閉包括:通過調用Runtime.halt或者在操作系統中“殺死”JVM進程(例如發送SIGKILL)。
關閉鉤子是指通過Runtime.addShutdownHook註冊的但尚未開始的線程。關閉鉤子可以用於實現服務或應用程序的清理工作,例如刪除臨時文件,或者清除無法由操作系統自動清除的資源。
由於關閉鉤子將併發執行,因此在關閉日誌文件時可能導致其他需要日誌服務的關閉鉤子產生問題。爲了避免這種情況,關閉鉤子不應該依賴於那些可能被應用程序或其他關閉鉤子關閉的服務。實現這種功能的一種方式是對所有服務使用同一個關閉鉤子,而不是每個服務使用一個不同的關閉鉤子,並且在該關閉鉤子中執行一系列的關閉操作。這確保了關閉操作在單個線程中串行執行,從而避免了在關閉操作之間出現競態條件或死鎖等問題。
//通過註冊一個關閉鉤子來停止日誌服務
public void start() {
Runtime.getRuntime().addShutdownHook(new Thread(){
public void run(){
try{
LogService.this.stop();
}catch(InterruptedException e){}
}
});
}
守護線程:
線程分爲兩種:普通線程和守護線程。在JVM啓動時創建的所有線程中,除了主線程以外,其他的線程都是守護線程(例如垃圾回收器以及其他執行輔助工作的線程)。當創建一個新線程時,新線程將繼承創建它的線程的守護狀態,因此在默認情況下,主線程創建的所有線程都是普通線程。
當一個線程退出時,JVM會檢查其他正在運行的線程,如果這些線程都是守護線程,那麼JVM會正常退出操作。當JVM停止時,所有仍然存在的守護線程都將被拋棄——既不會執行finally代碼塊,也不會執行回捲棧,而JVM只是直接退出。
我們應儘可能少地使用守護線程——很少有操作能夠在不進行清理的情況下被安全地拋棄。特別是,如果在守護線程中執行可能包含I/O操作的任務,那麼將是一種危險的行爲。守護線程最好用於執行“內部”任務,例如週期性地從內存的緩存中移除逾期的數據。此外,守護線程通常不能用來替代應用程序管理程序中各個服務的生命週期。
終結器:
當不再需要內存資源時,可以通過垃圾回收器來回收它們。但對於其他一些資源,例如文件句柄或套接字句柄,當不再需要它們時,必須顯式地交還給操作系統。爲了實現這個功能,垃圾回收器對那些定義了finalize方法的對象會進行特殊處理:在回收器釋放它們後,調用它們的finalize方法,從而保證一些持久化的資源被釋放。
由於終結器可以在某個可以由JVM管理的線程中運行,因此終結器訪問的任何狀態都可能被多個線程訪問,這樣就必須對其訪問操作進行同步。終結器並不能保證它們將在何時運行甚至是否會運行,並且複雜的終結器通常還會在對象上產生巨大的性能開銷。要編寫正確的終結器是非常困難的。在大多數情況下,通過使用finally代碼塊和顯式的close方法,能夠比使用終結器更好地管理資源。唯一的例外情況在於:當需要管理對象,並且該對象持有的資源是通過本地方法獲得的。基於這些原因以及其他一些原因,我們要儘量避免編寫或使用包含終結器的類(除非是平臺庫中的類)。避免使用終結器。
第八章:線程池的使用
8.1 在任務與執行策略之間的隱性耦合
有些類型的任務需要明確地指定執行策略:
- 依賴性任務。
- 使用線程封閉機制的任務。
- 對響應時間敏感的任務。
- 使用ThreadLocal的任務。
只有當任務都是同類型的並且相互獨立時,線程池的性能才能達到最佳。如果將運行時間較長的與運行時間較短的任務混合在一起,那麼除非線程池很大,否則將可能造成“擁塞”。如果提交的任務依賴於其他任務,那麼除非線程池無限大,否則將可能造成死鎖。
在一些任務中,需要擁有或排除某種特定的執行策略。如果某些任務依賴於其他的任務,那麼會要求線程池足夠大,從而確保它們依賴的任務不會被放入等待隊列中或被拒絕,而採用線程封閉機制的任務需要串行執行。通過將這些需求寫入文檔,將來的代碼維護人員就不會由於使用了某種不合適的執行策略而破壞安全性或活躍性。
/**
* ThreadDeadlock
* <p/>
* Task that deadlocks in a single-threaded Executor
*/
public class ThreadDeadlock {
ExecutorService exec = Executors.newSingleThreadExecutor();
public class LoadFileTask implements Callable<String> {
private final String fileName;
public LoadFileTask(String fileName) {
this.fileName = fileName;
}
public String call() throws Exception {
// Here's where we would actually read the file
return "";
}
}
public class RenderPageTask implements Callable<String> {
public String call() throws Exception {
Future<String> header, footer;
header = exec.submit(new LoadFileTask("header.html"));
footer = exec.submit(new LoadFileTask("footer.html"));
String page = renderBody();
// Will deadlock -- task waiting for result of subtask
return header.get() + page + footer.get();
}
private String renderBody() {
// Here's where we would actually render the page
return "";
}
}
}
每當提交了一個有依賴性的Executor任務時,要清楚地知道可能會出現線程“飢餓”死鎖,因此需要在代碼或配置Executor的配置文件中記錄線程池的大小限制或配置限制。
有一項技術可以緩解執行時間較長的任務造成的影響,即限定任務等待資源的時間,而不要無限制地等待。在平臺類庫的大多數可阻塞方法中,都同時定義了限時版本和無限時版本。
8.2 設置線程池的大小
要想正確地設置線程池的大小,必須分析計算環境、資源預算和任務的特性。對於計算密集型的任務,在擁有N個處理器的系統上,當線程池的大小爲N+1時,通常能實現最優的利用率。對於包含I/O操作或者其他阻塞操作的任務,由於線程並不會一直執行,因此線程池的規模應該更大。
可以通過Runtime來獲得CPU的數目:
int N_CPUS = Runtime.getRuntime().availableProcessors();
當然,CPU週期並不是唯一影響線程池大小的資源,還包括內存、文件句柄、套接字句柄和數據庫連接等。
8.3 配置ThreadPoolExecutor
基本大小也就是線程池的目標大小,即在沒有任務執行時線程池的大小,並且只有在工作隊列滿了的情況下才會創建超出這個數量的線程。在創建ThreadPoolExecutor初期,線程並不會立即啓動,而是等到有任務提交時纔會啓動,除非調用prestartAllCoreThreads。
基本的任務排隊方法有3種:無界隊列、有界隊列和同步移交(Synchronous Handoff)。
SynchronousQueue不是一個真正的隊列,而是一種在線程之間進行移交的機制。要將一個元素放入SynchronousQueue中,必須有另一個線程正在等待接受這個元素。否則根據飽和策略,這個任務將被拒絕。使用直接移交更高效。只有當線程池是無界的或者可以拒絕任務時,SynchronousQueue纔有實際價值。
對於Executor,newCachedThreadPool工廠方法是一種很好的默認選擇。它能提供比固定大小的線程池更好的排隊性能,這種性能的差異是由於使用了SynchronousQueue(在Java 6中提供了一個新的非阻塞算法來替代SynchronousQueue,該算法把Executor基準的吞吐量提高了3倍)。
只有當任務相互獨立時,爲線程池或工作隊列設置界限纔是合理的。如果任務之間存在依賴性,那麼有界的線程池或隊列就可能導致線程“飢餓”死鎖問題。
飽和策略:
當有界隊列被填滿後,飽和策略開始發揮作用。ThreadPoolExecutor的飽和策略可以通過調用setRejectedExecutionHandler來修改。如果某個任務被提交到一個已被關閉的Executor時,也會用到飽和策略。JDK提供了幾種不同的RejectedExecutionHandler實現,每種實現都包含有不同的飽和策略:AbortPolicy、CallerRunsPolicy、DiscardPolicy和DiscardOldestPolicy。
創建一個固定大小的線程池,並採用有界隊列及“調用者運行”飽和策略:
ThreadPoolExecutor executor = new ThreadPoolExecutor(
N_THREADS, N_THREADS, 0L,
TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>(CAPACITY)
);
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
每當線程池需要創建一個線程時,都是通過線程工廠方法來完成的。
在調用完ThreadPoolExecutor的構造函數後,仍然可以通過設置函數(Setter)來修改大多數傳遞給它的構造函數的參數。如果Executor是通過Executors中的某個(newSingleThreadExecutor除外)工廠方法創建的,那麼可以將結果的類型轉換爲ThreadPoolExecutor以訪問設置器。
ExecutorService exec = Executors.newCachedThreadPool();
if(exec instanceof ThreadPoolExecutor)
((ThreadPoolExecutor)exec).setCorePoolSize(10);
else
throw new AssertionError("Oops, bad assumption");
在Executors中包含一個unconfigurableExecutorService工廠方法,該方法對一個現有的ExecutorService進行包裝,使其只暴露出ExecutorService的方法,因此不能對它進行配置。newSingleThreadExecutor返回按這種方式封裝的ExecutorService,而不是最初的ThreadPoolExecutor。你可以在自己的Executor中使用這項技術以防止執行策略被修改。如果將ExecutorService暴露給不信任的代碼,又不希望對其進行修改,就可以通過unconfigurableExecutorService來包裝它。
ThreadPoolExecutor是可擴展的,它提供了幾個可以在子類化中改寫的方法:beforeExecute、afterExecute和terminated,這些方法可以用於擴展ThreadPoolExecutor的行爲。無論任務是從run中正常返回,還是拋出一個異常而返回,afterExecute都會被調用。如果任務完成後帶有一個Error,那麼就不會調用afterExecute。如果beforeExecute拋出一個RuntimeException,那麼任務將不被執行,並且afterExecute也不會被調用。
/**
* TimingThreadPool
* <p/>
* Thread pool extended with logging and timing
*/
public class TimingThreadPool extends ThreadPoolExecutor {
public TimingThreadPool() {
super(1, 1, 0L, TimeUnit.SECONDS, null);
}
private final ThreadLocal<Long> startTime = new ThreadLocal<Long>();
private final Logger log = Logger.getLogger("TimingThreadPool");
private final AtomicLong numTasks = new AtomicLong();
private final AtomicLong totalTime = new AtomicLong();
protected void beforeExecute(Thread t, Runnable r) {
super.beforeExecute(t, r);
log.fine(String.format("Thread %s: start %s", t, r));
startTime.set(System.nanoTime());
}
protected void afterExecute(Runnable r, Throwable t) {
try {
long endTime = System.nanoTime();
long taskTime = endTime - startTime.get();
numTasks.incrementAndGet();
totalTime.addAndGet(taskTime);
log.fine(String.format("Thread %s: end %s, time=%dns",
t, r, taskTime));
} finally {
super.afterExecute(r, t);
}
}
protected void terminated() {
try {
log.info(String.format("Terminated: avg time=%dns",
totalTime.get() / numTasks.get()));
} finally {
super.terminated();
}
}
}
如果循環中的迭代操作都是獨立的,並且不需要等待所有的迭代操作都完成再繼續進行(並且每個迭代操作執行的工作量比管理一個新任務時帶來的開銷更多),那麼就可以使用Executor將串行循環轉化爲並行循環。
void processInParallel(Executor exec, List<Element> elements){
for(final Element e : elements)
exec.execute(new Runnable(){
public void run(){ process(e); }
});
}
第九章:圖形用戶界面應用程序
9.1 爲什麼GUI是單線程的
在多線程的GUI框架中更容易發生死鎖問題。
Swing的單線程規則是:Swing中的組件以及模型只能在這個事件分發線程中進行創建、修改以及查詢。
與所有的規則相同,這個規則也存在一些例外情況。單線程規則的一些例外情況包括:
- SwingUtilities.isEventDispatchThread,用於判斷當前線程是否是事件線程。
- SwingUtilities.invokeLater,該方法可以將一個Runnable任務調度到事件線程中執行(可以從任意線程中調用)。
- SwingUtilities.invokeAndWait,該方法可以將一個Runnable任務調度到事件線程中執行,並阻塞當前線程直到任務完成(只能從非GUI線程中調用)。
- 所有將重繪(Repaint)請求或重生效(Revalidation)請求插入隊列的方法(可以從任意線程中調用)。
- 所有添加或移除監聽器的方法(這些方法可以從任意線程中調用,但監聽器本身一定要在事件線程中調用)。
/**
* SwingUtilities
* <p/>
* Implementing SwingUtilities using an Executor
*/
public class SwingUtilities {
private static final ExecutorService exec =
Executors.newSingleThreadExecutor(new SwingThreadFactory());
private static volatile Thread swingThread;
private static class SwingThreadFactory implements ThreadFactory {
public Thread newThread(Runnable r) {
swingThread = new Thread(r);
return swingThread;
}
}
public static boolean isEventDispatchThread() {
return Thread.currentThread() == swingThread;
}
public static void invokeLater(Runnable task) {
exec.execute(task);
}
public static void invokeAndWait(Runnable task)
throws InterruptedException, InvocationTargetException {
Future f = exec.submit(task);
try {
f.get();
} catch (ExecutionException e) {
throw new InvocationTargetException(e);
}
}
}
可以將Swing的事件線程視爲一個單線程的Executor,它處理來自事件隊列的任務。
/**
* GuiExecutor
* <p/>
* Executor built atop SwingUtilities
*/
public class GuiExecutor extends AbstractExecutorService {
// Singletons have a private constructor and a public factory
private static final GuiExecutor instance = new GuiExecutor();
private GuiExecutor() {
}
public static GuiExecutor instance() {
return instance;
}
public void execute(Runnable r) {
if (SwingUtilities.isEventDispatchThread())
r.run();
else
SwingUtilities.invokeLater(r);
}
public void shutdown() {
throw new UnsupportedOperationException();
}
public List<Runnable> shutdownNow() {
throw new UnsupportedOperationException();
}
public boolean awaitTermination(long timeout, TimeUnit unit)
throws InterruptedException {
throw new UnsupportedOperationException();
}
public boolean isShutdown() {
return false;
}
public boolean isTerminated() {
return false;
}
}
9.2 短時間的GUI任務
爲了簡便,短時間的任務可以把整個操作都放在事件線程中執行,而對於長時間的任務,則應該將某些操作放到另一個線程中執行。
Swing將大多數可視化組件都分爲兩個對象,即模型對象與視圖對象。在模型對象中保存的是將被顯示的數據,而在視圖對象中則保存了控制顯示方式的規則。
9.3 長時間的GUI任務
通過Future來表示一個長時間的任務,可以極大地簡化取消操作的實現。在FutureTask中也有一個done方法同樣有助於實現完成通知。
/**
* BackgroundTask
* <p/>
* Background task class supporting cancellation, completion notification, and progress notification
*/
public abstract class BackgroundTask <V> implements Runnable, Future<V> {
private final FutureTask<V> computation = new Computation();
private class Computation extends FutureTask<V> {
public Computation() {
super(new Callable<V>() {
public V call() throws Exception {
return BackgroundTask.this.compute();
}
});
}
protected final void done() {
GuiExecutor.instance().execute(new Runnable() {
public void run() {
V value = null;
Throwable thrown = null;
boolean cancelled = false;
try {
value = get();
} catch (ExecutionException e) {
thrown = e.getCause();
} catch (CancellationException e) {
cancelled = true;
} catch (InterruptedException consumed) {
} finally {
onCompletion(value, thrown, cancelled);
}
};
});
}
}
protected void setProgress(final int current, final int max) {
GuiExecutor.instance().execute(new Runnable() {
public void run() {
onProgress(current, max);
}
});
}
// Called in the background thread
protected abstract V compute() throws Exception;
// Called in the event thread
protected void onCompletion(V result, Throwable exception,
boolean cancelled) {
}
protected void onProgress(int current, int max) {
}
// Other Future methods just forwarded to computation
public boolean cancel(boolean mayInterruptIfRunning) {
return computation.cancel(mayInterruptIfRunning);
}
public V get() throws InterruptedException, ExecutionException {
return computation.get();
}
public V get(long timeout, TimeUnit unit)
throws InterruptedException,
ExecutionException,
TimeoutException {
return computation.get(timeout, unit);
}
public boolean isCancelled() {
return computation.isCancelled();
}
public boolean isDone() {
return computation.isDone();
}
public void run() {
computation.run();
}
}
9.4 共享數據模型
只要阻塞操作不會過度地影響響應性,那麼多個線程操作同一份數據的問題都可以通過線程安全的數據模型來解決。
如果在程序中既包含用於表示的數據模型,又包含應用程序特定的數據模型,那麼這種應用程序就被稱爲擁有一種分解模型設計。如果一個數據模型必須被多個線程共享,而且由於阻塞、一致性或複雜度等原因而無法實現一個線程安全的模型時,可以考慮使用分解模型設計。
9.5 其他形式的單線程子系統
線程封閉不僅僅可以在GUI中使用,每當某個工具需要被實現爲單線程子系統時,都可以使用這項技術。有時候,當程序員無法避免同步或死鎖等問題時,也將不得不使用線程封閉。
第三部分:活躍性、性能與測試
本文到此處告一段落…
P183