第五章:構建塊
平臺類庫包含了一個併發構建塊的豐富集合,比如線程安全容器和多種同步工具(Synchronizer
)。
Synchronizer
用來調節相互協作的線程間的控制流。
同步容器
同步容器類包括兩部分,一個 是Vector
和HashTable
,它們是早期JDK的一部分;另一個是它們的同系容器,在JDK1.2中才被加入的同步包裝(wrapper
)類。
這些類是由Collections.synchronizedXxx
工廠方法創建的。
這些類通過封裝它們的狀態,並對每一個公共方法進行同步而實現了線程安全,這樣一次只有一個線程能訪問容器的狀態。
同步容器中出現的問題
同步容器都是線程安全的。但是對於複合操作,有時你可能需要使用額外的客戶端加鎖(client-side locking)進行保護。
通常對容器的複合操作包括:迭代(反覆獲取元素,直到獲得容器中的最後一個元素)、導航(根據一定的順序尋找下一個元素)以及條件運算,比如“缺少即加入”(put-if-absent),檢查Map中是否存在關鍵字K,如果沒有,就加入mapping (K,V)。
在一個同步的容器中,這些複合操作即使沒有客戶端加鎖的保護,技術上也是線程安全的,但是當其他線程能夠併發修改容器的時候,它們就可能不會按照你期望的方式工作了。
/**
* 操作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) {
synchronized (list) {
int lastIndex = list.size() - 1;
return list.get(lastIndex) ;
}
}
public static void deleteLast(Vector list) {
synchronized (list) {
int lastIndex = list.size() - 1;
list.remove(lastIndex);
}
}
併發容器
Java5.0通過提供幾種併發的容器類來改進同步容器。併發容器是爲多線程併發訪問而設計的。
- Java 5.0添加了
ConcurrentHashMap
,來替代同步的哈希Map實現 - 當多數操作爲讀取操作時,
CopyOnWriteArrayList
來替代List相應的同步實現
新的ConcurrentMap
接口加入了對常見覆合操作的支持,比如“缺少即加入(put-if-absent) ”、替換和條件刪除。
用併發容器替換同步容器,這種作法以有很小風險帶來了可擴展性顯著的提高。
Java 5.0同樣添加了兩個新的容器類型:Queue
和BlockingQueue
。
Queue
用來臨時保存正在等待被進一步處理的一系列元素。基於Queue
,有一系列的具體實現。BlockingQueue
擴展了Queue
,增加了可阻塞的插入和獲取操作。阻塞隊列在生產者消費者設計中非常有用。
正像ConcurrentHashMap
作爲同步的哈希Map的一個併發替代品,Java 6加入了ConcurrentSkipListMap
和ConcurrentSkipListset
,用來作爲同步的SortedMap
和SortedSet
的併發替代品(比如用synchronizedMap
包裝的TreeMap
或TreeSet
)。
ConcurrentHashMap
ConcurrentHashMap
和HashMap
一樣是一個哈希表,但是它使用完全不同的鎖策略,可以提供更好的併發性和可伸縮性。
ConcurrentHashMap
使用一個更加細化的鎖機制,名叫分離鎖。這個機制允許更深層次的共享訪問。任意數量的讀線程可以併發訪問Map,讀者和寫者也可以併發訪問Map,並且有限數量的寫線程還可以併發修改Map。結果是,爲併發訪問帶來更高的吞吐量,同時幾乎沒有損失單個線程訪問的性能。
阻塞隊列和生產者-消費者模式
阻塞隊列(BlockingQueue
)提供了可阻塞的put
和take
方法,它們與可定時的offer
和poll
是等價的。
阻塞隊列支持生產者-消費者設計模式。一個生產者-消費者設計分離了“識別需要完成的工作”和“執行工作”。該模式不會發現一個工作便立即處理,而是把工作置入一個任務(“to do”)清單中,以備後期處理。
生產者和消費者以不同的或者變化的速度生產和消費着數據,生產者-消費者模式將這些活動解耦,因而簡化了工作負荷的管理。
雙端隊列和工作竊取
Java 6同樣新增了兩個容器類型,Deque
(發音是deck)和BlockingDeque
,它們分別擴展了Queue
和BlockingQueue
。
Deque
是一個雙端隊列,允許高效地在頭和尾分別進行插入和移除。實現它們的是ArrayDeque
和LinkedBlockingDeque
。
正如阻塞隊列適用於生產者-消費者模式一樣,雙端隊列使它們自身與一種叫做工作竊取(work stealing) 的模式相關聯。
一個消費者生產者設計中,所有的消費者只共享一個工作隊列;在竊取工作的設計中,每一個消費者都有一個自己的雙端隊列。如果一個消費者完成了自己雙端隊列中的全部工作,它可以偷取其他消費者的雙端隊列中的末尾任務。
因爲工作者線程並不會競爭一個共享的任務隊列,所以竊取工作模試比傳統的生產者-消費者設計有更佳的可伸縮性;大多數時候它們訪問自己的雙端隊列,減少競爭。
當一個工作者必須要訪問另一個隊列時, 它會從尾部截取,而不是從頭部,從而進一步降低對雙端隊列的爭奪。
Synchronizer
Synchronizer
是一個對象,它根據本身的狀態調節線程的控制流。
阻塞隊列可以扮演一個Synchronizer
的角色;其他類型的Synchronizer
包括信號量( semaphore)、關卡( barrier)以及閉鎖(latch)。
在平臺類庫中存在一些Synchronizer
類;如果這些不能滿足你的需要,你同樣可以按照第14章裏描述的那樣,創建一個你自己的Synchronizer
。
所有Synchronizer
都享有類似的結構特性:它們封裝狀態,而這些狀態決定着線程執行到在某一點時是通過還是被迫等待;它們還提供操控狀態的方法,以及高效地等待Synchronizer
進入到期望狀態的方法。
閉鎖
閉鎖(latch)是一種Synchronizer
,它可以延遲線程的進度直到線程到達終止(terminal)狀態。
一個閉鎖工作起來就像一道大門:直到閉鎖達到終點狀態之前,門一直是關閉的,沒有線程能夠通過,在終點狀態到來的時候,門開了,允許所有線程都通過。
閉鎖可以用來確保特定活動直到其他的活動完成後才發生,比如:
- 確保一個計算不會執行,直到它需要的資源被初始化。一個二元閉鎖(兩個狀態)可以用來表達“資源R已經被初始化”,並且所有需要用到R的活動首先都要在閉鎖中等待。
- 確保一個服務不會開始,直到它依賴的其他服務都已經開始。每一個服務會包含一個相關的二元閉鎖;開啓服務S會首先開始等待閉鎖s中所依賴的其他服務, 在啓動結束後,會釋放閉鎖S,這樣所有依賴S的服務也可以開始處理了。
- 等待,直到活動的所有部分都爲繼續處理作好充分準備,比如在多玩家的遊戲中的所有玩家是否都準備就緒。這樣的閉鎖會在所有玩家準備就緒時,達到終點狀態。
CountDownlatch
是一個靈活的閉鎖實現,用於上述各種情況;允許一個或多個線程等待一個事件集的發生。
閉鎖的狀態包括一個計數器, 初始化爲一個正數,用來表現需要等待的事件數。
countDown方法對計數器做減操作,表示一個事件已經發生了,而await方法等待計數器達到零,此時所有需要等待的事件都已發生。如果計數器入口時值爲非零,await方法會一直阻塞直到計數器爲零,或者等待線程中斷以及超時。
FutureTask
FutureTask
同樣可以作爲閉鎖。(FutureTask
的實現描述了一個抽象的可攜帶結果的計算)。
FutureTask
的計算是通過Callable
實現的,它等價於一個可攜帶結果的Runnable
,並且有3個狀態:等待、運行和完成。
完成包括所有計算以任意的方式結束,包括正常結束、取消和異常。一旦FutureTask
進入完成狀態,它會永遠停止在這個狀態上。
Future.get
的行爲依賴於任務的狀態。如果它已經完成,get
可以立刻得到返回結果,否則會被阻塞直到任務轉入完成狀態,然後會返回結果或者拋出異常。
FutureTask
把計算的結果從運行計算的線程傳送到需要這個結果的線程:FutureTask
的規約保證了這種傳遞建立在結果的安全發佈基礎之上。
/**
* 使用FutureTask預載稍後需要的數據
*/
public class Preloader {
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(cause);
}
}
}
關卡
關卡(barrier
)類似於閉鎖,它們都能夠阻塞一組線程, 直到某些事件發生。
其中關卡與閉鎖關鍵的不同在於,所有線程必須同時到達關卡點,才能繼續處理。
閉鎖等待的是事件;關卡等待的是其他線程。
關卡實現的協議,就像一些家庭成員指定商場中的集合地點:”我們每個人6:00在麥當勞見,到了以後不見不散,之後我們再決定接下來做什麼。“
CyclicBarrier
允許一個給定數量的成員多次集中在一個關卡點,這在並行迭代算法中非常有用,這個算法會把一個問題拆分成一系列相互獨立的子問題。
當線程到達關卡點時,調用await,await會被阻塞,直到所有線程都到達關卡點。如果所有線程都到達了關卡點,關卡就被成功地突破,這樣所有線程都被釋放,關卡會重置以備下一次使用。
如果對await的調用超時,或者阻塞中的線程被中斷,那麼關卡就被認爲是失敗的,所有對await未完成的調用都通過BrokenBarrierException
終止。
如果成功地通過關卡,await
爲每一個線程返回一個唯一的到達索引號, 可以用它來“選舉”產生一個領導, 在下一次迭代中承擔一些特殊工作。
/**
* 在一個細胞的 自動系統中用CyclicBarrier協調計箅
*/
public class CellularAutomata {
private final Board mainBoard;
private final CyclicBarrier barrier;
private final Worker[] workers;
public CellularAutomata(Board board) {
this.mainBoard = board;
int count = Runtime.getRuntime().availableProcessors();
this.barrier = new CyclicBarrier(count, () -> mainBoard.commitNewValues());
this.workers = new Worker[count];
for (int i = 0; i < count; i++) {
workers[i] = new Worker(mainBoard.getSubBoard(count, i));
}
}
private class Worker implements Runnable {
private final Board board;
public Worker(Board board) {
this.board = board;
}
@Override
public void run() {
while (!board.hasConverged()) {
for (int x = 0; x < board.getMaxX(); x++) {
for (int y = 0; y < board.getMaxY(); y++) {
board.setNewValue(x, y, computeValue(x, y));
}
}
try {
barrier.await();
} catch (InterruptedException ex) {
return;
} catch (BrokenBarrierException ex) {
return;
}
}
}
public void start() {
for (int i = 0; i < workers.length; i++) {
new Thread(workers[i]).start();
}
mainBoard.waitForConvergence();
}
}
}