同步工具類可以是任意一個對象,只要它可以根據自身的狀態來協調線程的控制流。阻塞隊列可以作為同步工具類,其他類型的同步工具類還包括信號量(Semaphore)、柵欄(Barrier)以及閉鎖。在平台類庫中還包含一些其他同步工具類,如果還是不能滿足需要,我們可以創建自己的同步工具類。
一、閉鎖
閉鎖可以延遲線程的進度直到其達到終止狀態。閉鎖可以用來確保某些活動直到其他活動都完成后才繼續執行。例如:
某個計算在其需要的資源都初始化完成之后執行;
某個服務在其所有依賴的服務都啟動之后才啟動;
游戲中所有的玩家都就緒才繼續執行。
CountDownLatch是一種靈活的閉鎖實現,它可以使一個或多個線程等待一組時間發生。閉鎖狀態包括一個計數器,該計數器初始化為一個正數,表示需要等待的事件數量,countDown方法表示遞減計數器,表示一個事件發生了,而await方法等待直到計數器為0,表示所有事件都已經發生。如果計數器的值非零,那么就會一直等待下去,或者等待中被打斷,或者超時。
例子(計算所有線程運行時間):package thread.semaphore;
import java.util.concurrent.CountDownLatch;
public class TestHarness {
public static long timeTask(int nThreads, final Runnable task) throws InterruptedException {
final CountDownLatch startGate = new CountDownLatch(1);
final CountDownLatch endGate = new CountDownLatch(nThreads);
for (int i=0; i
Thread thread = new Thread() {
@Override
public void run() {
try {
startGate.await();
task.run();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
endGate.countDown();
}
}
};
thread.start();
}
long start = System.currentTimeMillis();
startGate.countDown();
endGate.await();
long end = System.currentTimeMillis();
return end-start;
}
public static void main(String[] args) throws InterruptedException {
long time = timeTask(5, new Runnable() {
@Override
public void run() {
try {
Thread.sleep((int)(500 * Math.random()));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
System.out.println("all task use " + time + "ms");
}
}
上例中使用了兩個閉鎖,一個起始門(startGate),一個結束門(endGate)。起始門的計數器值初始化為1,結束門是線程數,每個線程首先要做的就是在起始門上等待多有的線程都就緒后才開始執行。而每個線程最后要做的事就是調用結束門countDown方法減1,這能高效的等待所有的線程都工作完成,這樣可以統計消耗的時間。
其他方法
如果有某個線程處理的比較慢,我們不可能讓主線程一直等待,所以我們可以使用另外一個帶指定時間的await方法,await(long time, TimeUnit unit): 這個方法等待特定時間后,就會不再阻塞當前線程。join也有類似的方法。
注意:計數器必須大於等於0,只是等於0時候,計數器就是零,調用await方法時不會阻塞當前線程。CountDownLatch不可能重新初始化或者修改CountDownLatch對象的內部計數器的值。
二、信號量
計數信號量(Counting Semaphore)用來控制同時訪問某個特定資源的操作數量。計數信號量還可以用來實現某種資源池(如:數據庫連接池),或者對容器施加邊界。
Semaphore中管理着一組虛擬的許可(permit),許可的數量可以通過構造器來指定。在執行操作時可以先獲取許可(只要還有剩余的許可),並在使用之后釋放許可。如果沒有許可,那么aquire將阻塞指定獲取許可(或者直到被中斷或者操作超時)。release將返回一個許可給信號量。計算信號量的一種簡化形式就是二值信號量,即初始值為1的Semaphore。二值信號量可以用作互斥體(mutex),並具備不可重入的語義:誰擁有這個唯一的許可,誰就擁有了互斥鎖。
例子(流量控制):
要讀取幾萬個文件的數據,因為都是IO密集型任務,我們可以啟動幾十個線程並發的讀取,但是如果讀到內存后,還需要存儲到數據庫中,而數據庫的連接數只有10個,這時我們必須控制只有十個線程同時獲取數據庫連接保存數據,否則會報錯無法獲取數據庫連接。public class SemaphoreTest {
private static final int THREAD_COUNT = 30;
private static ExecutorService threadPool = Executors.newFixedThreadPool(THREAD_COUNT);
private static Semaphore s = new Semaphore(10);
public static void main(String[] args) {
for (int i = 0; i < THREAD_COUNT; i++) {
threadPool.execute(new Runnable() {
@Override
public void run() {
try {
s.acquire();
System.out.println("save data");
s.release();
} catch (InterruptedException e) {
}
}
});
}
threadPool.shutdown();
}
}
其他方法:
int availablePermits() :返回此信號量中當前可用的許可證數。
int getQueueLength():返回正在等待獲取許可證的線程數。
boolean hasQueuedThreads() :是否有線程正在等待獲取許可證。
void reducePermits(int reduction) :減少reduction個許可證。是個protected方法。
Collection getQueuedThreads() :返回所有等待獲取許可證的線程集合。是個protected方法。
三、柵欄(同步屏障)
1. CyclicBarrier
柵欄(Barrier)類似於閉鎖,它能阻塞一組線程直到某個時間發生。柵欄和閉鎖的區別在於,所有線程必須都到達柵欄位置之后才能繼續執行。閉鎖用於等待事件,二柵欄用於等待其他線程。
閉鎖是一次性操作,一旦進入終止狀態就不能重置。CyclicBarrier可以使一定數量的線程反復在柵欄位置匯集,它在並行迭代算法中非常有用:這種算法通常將一個問題拆分成多個互不相關的子問題。當線程執行到柵欄位置時將調用await方法等待其他線程,這個方法阻塞直到所有線程都到達柵欄位置。當所有線程都到達柵欄位置,那么柵欄打開,所有線程都被釋放。而柵欄將被重置以便下次使用。如果await被中斷或者超時,那么柵欄被認為是打破了,所有線程的await都將被終止並拋出BrokenBarrierException。如果成功的通過柵欄,那么await將為每個線程返回一個唯一的到達索引號,我們可以利用這些索引來“選舉”產生一個領導線程,並在下一次迭代中由該領導線程執行一些特殊的工作。
CyclicBarrier還可以利用構造函數傳遞一個Runnable,當成功通過柵欄時會執行它,但在阻塞線程被釋放前不會執行。
例子(10個人去春游,規定達到一個地點后才能繼續前行)package thread.semaphore;
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;
public class CyclicBarrierWorker implements Runnable {
private int id;
private CyclicBarrier cyclicBarrier;
public CyclicBarrierWorker(int id, CyclicBarrier cyclicBarrier) {
this.id = id;
this.cyclicBarrier = cyclicBarrier;
}
@Override
public void run() {
try {
System.out.println(id + " th people wait, waitings " + cyclicBarrier.getNumberWaiting());
int returnIndex = cyclicBarrier.await();// 大家等待最后一個線程到達
System.out.println(id + " th people go, returnIndex:" + returnIndex);
} catch (InterruptedException | BrokenBarrierException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
int num = 10;
CyclicBarrier cyclicBarrier = new CyclicBarrier(num, new Runnable() {
@Override
public void run() {
System.out.println("go on together!");
}
});
for (int i=1; i<=num; i++) {
new Thread(new CyclicBarrierWorker(i, cyclicBarrier)).start();
}
}
}
運行結果:2 th people wait, waitings 1
4 th people wait, waitings 1
3 th people wait, waitings 3
5 th people wait, waitings 4
6 th people wait, waitings 5
7 th people wait, waitings 6
8 th people wait, waitings 6
9 th people wait, waitings 7
10 th people wait, waitings 9
go on together!
10 th people go, returnIndex:0
2 th people go, returnIndex:8
4 th people go, returnIndex:7
3 th people go, returnIndex:6
1 th people go, returnIndex:9
7 th people go, returnIndex:3
9 th people go, returnIndex:1
6 th people go, returnIndex:4
5 th people go, returnIndex:5
8 th people go, returnIndex:2
2.Exchanger(兩個線程進行數據交換)
另一種柵欄是Exchanger,它是一種兩方(two-party)柵欄,各方在柵欄位置互換數據。當兩方執行不對稱操作時Exchanger會非常有用,例如一個線程向緩存中寫數據,另一線程讀數據,這兩個線程可以使用Exchanger匯合,並將滿的緩沖區和空的緩沖區互換。它提供一個同步點,在這個同步點兩個線程可以交換彼此的數據。這兩個線程通過exchange方法交換數據, 如果第一個線程先執行exchange方法,它會一直等待第二個線程也執行exchange,當兩個線程都到達同步點時,這兩個線程就可以交換數據,將本線程生產出來的數據傳遞給對方。
例子(校對工作):public class ExchangerTest {
private static final Exchanger exgr = new Exchanger();
private static ExecutorService threadPool = Executors.newFixedThreadPool(2);
public static void main(String[] args) {
threadPool.execute(new Runnable() {
@Override
public void run() {
try {
String A = "銀行流水A";// A錄入銀行流水數據
exgr.exchange(A);
} catch (InterruptedException e) {
}
}
});
threadPool.execute(new Runnable() {
@Override
public void run() {
try {
String B = "銀行流水B";// B錄入銀行流水數據
String A = exgr.exchange("B");
System.out.println("A和B數據是否一致:" + A.equals(B) + ",A錄入的是:"
+ A + ",B錄入是:" + B);
} catch (InterruptedException e) {
}
}
});
threadPool.shutdown();
}
}
運行結果:A和B數據是否一致:false,A錄入的是:銀行流水A,B錄入是:銀行流水B
其他方法
如果兩個線程有一個沒有到達exchange方法,則會一直等待,如果擔心有特殊情況發生,避免一直等待,可以使用exchange(V x, long timeout, TimeUnit unit)設置最大等待時長。
參考:《java並發編程實戰》
並發編程網