同步工具類是指,能夠根據自身的狀態來協調線程的控制流的類,同步工具類的特徵是,它們封裝的一些狀態能夠決定執行同步工具類的線程是執行還是等待,此外還提供一些方法對狀態進行操作,以及一些方法用於高效地等待同步工具類進入到預期狀態。
1. 阻塞隊列
BlockingQueue,阻塞隊列不僅能作爲保存對象的容器,也是同步工具類,它能協調生產者消費者等線程之間的控制流,take、put、offer和poll能夠阻塞,知道隊列達到預期狀態。
2. 閉鎖
如果把阻塞隊列想象成工廠裏的工作流,閉鎖就好像在公司開會的集合過程。產品和程序員收到消息到辦公司開會,各自開始放下手頭的工作進入辦公室,當boss確定人員到齊,會議開始。
閉鎖是一種同步工具類,可以延遲線程的進度,知道其達到終止狀態。在開會集合裏,參與會議的人員就是線程,終止狀態是所有人員到期的條件。閉鎖可以確保某些活動直到一個動作都發生才繼續執行,例如:確保所有資源都被初始化才繼續執行;確保某個服務器以來的其它服務器都已經啓動才啓動;直達lol所有玩家都準備好才進入遊戲。
public class CountDown {
public static 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(){
@Override
public void run() {
try {
startGate.await();
try{
task.run();
}finally{
endGate.countDown();
}
} catch (InterruptedException ignore) {
}
}
};
t.start();
}
Long startTime = System.currentTimeMillis();
startGate.countDown();
endGate.await();
Long endTime = System.currentTimeMillis();
return endTime - startTime;
}
public static void main(String[] args) {
Runnable task = new Runnable(){
@Override
public void run() {
Thread thread = Thread.currentThread();
System.out.println(thread.getName()+" 進入辦公室");
}
};
try {
System.out.println("項目經理要求開會");
Long time = CountDown.timeTasks(10, task);
System.out.println("人員到齊,會議開始");
} catch (InterruptedException ignore) {
}
}
}
上面的代碼描述了開會的集合過程。
這裏提出一個疑問,CountDown.timeTasks的startGate和endGated用一個原子變量AtomicInteger也可以達到同樣的效果,爲什麼還要閉鎖?
講道理,使用原子變量或其他同步手段實現計數,如果只是做到“達到終止條件就繼續執行“是沒問題的,CountDownLatch底層是一個鏈表,鏈表的每個元素是一個node對象,所有調用await的操作會把一個新的node對象插入鏈表tail端,node還將調用的線程對象因此存在thread變量。當終止條件到達,是從head開始逐個喚醒線程的。也就是說,閉鎖不僅能實現“達到終止條件繼續執行“,還能做到“先進先出“。而且就線程訪問控制來說,CountDownLatch的封裝性也更好呀,杜絕了對外公佈計數器的風險。
class Node {
volatile Node prev;
volatile Node next;
volatile Thread thread;
Node nextWaiter;
...
}
+------+ prev +-----+ +-----+
head | | <---- | | <---- | | tail
+------+ +-----+ +-----+
3. FutureTask
FutureTask是一種特殊的閉鎖,它相當於CountDownLatch的計數爲1的情況。FutureTask增加了對運行中的線程的監控,還能夠從線程中顯式的傳遞數據到上層代碼。關於FutureTask的get方法在線程還未運行、正在運行時調用,調用者會阻塞,線程已經運行完成,則會返回結果或拋出異常。FutureTask相關的知識比較多,後面單獨講,這裏先挖個坑。
4. 信號量
計數信號量用來控制同時訪問某個資源的操作數量,或者同時執行某個操作的數量。用法上和阻塞隊列沒很大區別,信號量更專注於對訪問數量的控制,只管理着一組虛擬的許可,並不存放資源,這一點和阻塞隊列還是有別的。信號量的功能更加純粹,它能和任何一種容器結合使之變成有界阻塞容器。SemaphoreSet是一個用信號量封裝過的Set類,支持對線程訪問的控制。
Semaphore的acquire用來獲得許可,release用來釋放許可。
public class SemaphoreSet<T> {
private Set<T> set;
private Semaphore sem;
public SemaphoreSet(Set<T> set, Integer permits){
this.set = set;
this.sem = new Semaphore(permits);
}
public boolean add(T e) throws InterruptedException{
sem.acquire();
boolean wasAdd = false;
try{
wasAdd = set.add(e);
return wasAdd;
}finally{
if(!wasAdd)
sem.release();
}
}
public boolean remove(Object o){
boolean wasRemoved = set.remove(o);
if(wasRemoved)
sem.release();
return wasRemoved;
}
}
5. 柵欄
前面說的閉鎖能夠確保某些活動直到一個動作都發生才繼續執行,這裏必須要說明一下,當一個線程調用CountDownLatch的countDown,就會進入阻塞狀態,不管這個線程終止、中斷或是發生其他異常情況,對於CountDownLatch除了記錄又一個線程成果到達並不會關心這些,參與活動的線程的終止、中斷也不會影響到其他活動線程。
在一些活動線程之間業務上緊關聯的情況下,一個活動線程的失敗意味着整個活動已經沒意義,需要終止,閉鎖很可能佔有cpu運行沒有價值的代碼。柵欄因此孕育而生。
柵欄比閉鎖增加了四個特性:
1. 對await的調用超時,或者await阻塞的線程被中斷,那麼柵欄就被認爲是被打破了,所有阻塞的調用都將被終止並拋出BrokenBarrierException;
2. 如果成果地通過柵欄,await將爲每個線程返回一個唯一的到達索引號,利用這些索引號可以選擇一個“領導線程“,並在後續由它執行一些特殊的工作;
3. CyclicBarrier構造函數運行將一個Runnable對象存儲起來,當成功通過柵欄時會(在一個字任務線程中)執行它,但在阻塞線程被全部釋放前是不能執行的;
4. CyclicBarrier的官方註釋提到,“Cyclic“意味着柵欄時可以重複使用的,特別環保。
public class Barrier {
final int N;
final float[][] data;
final CyclicBarrier barrier;
class Worker implements Runnable {
int myRow;
Worker(int row) {
myRow = row;
}
public void run() {
while (!done()) {
processRow(myRow);
try {
barrier.await();
} catch (InterruptedException ex) {
return;
} catch (BrokenBarrierException ex) {
return;
}
}
}
}
public Barrier(float[][] matrix) {
data = matrix;
N = matrix.length;
Runnable barrierAction = new Runnable() {
public void run() {
mergeRows();
}
};
barrier = new CyclicBarrier(N, barrierAction);
List<Thread> threads = new ArrayList<Thread>(N);
for (int i = 0; i < N; i++) {
Thread thread = new Thread(new Worker(i));
threads.add(thread);
thread.start();
}
// wait until done
for (Thread thread : threads){
try {
thread.join();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
private boolean done(){
return false;
}
private void processRow(int row){}
private void mergeRows(){}
}
上面的Barrier類用於計算二維數組的和。這裏爲每行開了一個線程Worker
計算和,當所有計算都完成,調用barrierAction
計算最終的結果。
柵欄特別適合並行迭代算法中,將一個問題拆分成一些列互相獨立的子問題的情況。