類 | 作用 |
---|---|
Semaphore | 限制執行緒的數量 |
Exchanger | 兩個執行緒交換資料 |
CountDownLatch | 執行緒等待直到計數器減為 0 時開始工作 |
CyclicBarrier | 作用跟 CountDownLatch 類似,但是可以重複使用 |
Phaser | 增強的 CyclicBarrier |
Semaphore
Semaphore 翻譯過來是訊號的意思。顧名思義,這個工具類提供的功能就是多個執行緒彼此“傳訊號”。而這個“訊號”是一個int
型別的資料,也可以看成是一種“資源”。
// 傳入初始資源總數,預設情況下,是非公平的同步器
public Semaphore(int permits) {
sync = new NonfairSync(permits);
}
public Semaphore(int permits, boolean fair) {
sync = fair ? new FairSync(permits) : new NonfairSync(permits);
}
最主要的方法是 acquire 方法和 release 方法。acquire()
方法會申請一個 permit,而 release 方法會釋放一個 permit。當然,你也可以申請多個 acquire(int permits)
或者釋放多個 release(int permits)
。
每次 acquire,permits 就會減少一個或者多個。如果減少到了 0,再有其他執行緒來 acquire,那就要阻塞這個執行緒直到有其它執行緒 release permit 為止。
Semaphore 往往用於資源有限的場景中,去限制執行緒的數量。舉個例子,我想限制同時只能有 3 個執行緒在工作:
public class Main {
static class MyThread implements Runnable {
private final int value;
private final Semaphore semaphore;
public MyThread(int value, Semaphore semaphore) {
this.value = value;
this.semaphore = semaphore;
}
@Override
public void run() {
try {
// 獲取 permit
semaphore.acquire();
System.out.printf("當前執行緒是%d, 還剩%d個資源,還有%d個執行緒在等待%n",
value, semaphore.availablePermits(), semaphore.getQueueLength());
// 睡眠隨機時間,打亂釋放順序
Random random = new Random();
Thread.sleep(random.nextInt(1000));
System.out.printf("執行緒%d釋放了資源%n", value);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// 釋放 permit
semaphore.release();
}
}
}
public static void main(String[] args) {
Semaphore semaphore = new Semaphore(3);
for (int i = 0; i < 10; i++) {
new Thread(new MyThread(i, semaphore)).start();
}
}
}
Semaphore 預設的 acquire 方法是會讓執行緒進入等待佇列,且丟擲異常中斷。但它還有一些方法可以忽略中斷或不進入阻塞佇列:
// 忽略中斷
public void acquireUninterruptibly()
public void acquireUninterruptibly(int permits)
// 不進入等待佇列,底層使用CAS
public boolean tryAcquire
public boolean tryAcquire(int permits)
public boolean tryAcquire(int permits, long timeout, TimeUnit unit)
throws InterruptedException
public boolean tryAcquire(long timeout, TimeUnit unit)
Semaphore 內部有一個繼承了 AQS 的同步器 Sync,重寫了tryAcquireShared
方法。在這個方法裡,會去嘗試獲取資源。如果獲取失敗(想要的資源數量小於目前已有的資源數量),就會返回一個負數(代表嘗試獲取資源失敗)。然後當前執行緒就會進入 AQS 的等待佇列。
Exchanger
Exchanger 類用於兩個執行緒交換資料。它支援泛型,也就是說可以在兩個執行緒之間傳送任何資料。
public class Main {
public static void main(String[] args) throws InterruptedException {
Exchanger<String> exchanger = new Exchanger<>();
new Thread(() -> {
try {
System.out.println("這是執行緒A,得到了另一個執行緒的資料:"
+ exchanger.exchange("這是來自執行緒A的資料"));
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
// 當一個執行緒呼叫 exchange 方法後,會處於阻塞狀態,只有當另一個執行緒也呼叫了 exchange 方法,它才會繼續執行
System.out.println("這個時候執行緒A是阻塞的,在等待執行緒B的資料");
Thread.sleep(1000);
new Thread(() -> {
try {
System.out.println("這是執行緒B,得到了另一個執行緒的資料:"
+ exchanger.exchange("這是來自執行緒B的資料"));
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
}
}
根據 JDK 裡面註釋的說法,可以總結為一下特性:
- 此類提供對外的操作是同步的;
- 用於成對出現的執行緒之間交換資料;
- 可以視作雙向的同步佇列;
- 可應用於基因演算法、流水線設計等場景。
Exchanger 類還有一個有超時引數的方法,如果在指定時間內沒有另一個執行緒呼叫 exchange,就會丟擲一個超時異常。
public V exchange(V x, long timeout, TimeUnit unit)
Exchanger 是可以重複使用的。也就是說。兩個執行緒可以使用 Exchanger 在記憶體中不斷地再交換資料。
CountDownLatch
public class CountDownLatch {
private static final class Sync extends AbstractQueuedSynchronizer {
private static final long serialVersionUID = 4982264981922014374L;
// 計數值(count)實際上就是閉鎖需要等待的執行緒數量。這個值只能被設定一次,而且 CountDownLatch沒有提供任何機制去重新設定這個計數值
Sync(int count) {
setState(count);
}
int getCount() {
return getState();
}
protected int tryAcquireShared(int acquires) {
return (getState() == 0) ? 1 : -1;
}
protected boolean tryReleaseShared(int releases) {
// Decrement count; signal when transition to zero
for (;;) {
int c = getState();
if (c == 0)
return false;
int nextc = c-1;
if (compareAndSetState(c, nextc))
return nextc == 0;
}
}
}
private final Sync sync;
public CountDownLatch(int count) {
if (count < 0) throw new IllegalArgumentException("count < 0");
this.sync = new Sync(count);
}
// 等待
public void await() throws InterruptedException {
sync.acquireSharedInterruptibly(1);
}
// 超時等待
public boolean await(long timeout, TimeUnit unit)
throws InterruptedException {
return sync.tryAcquireSharedNanos(1, unit.toNanos(timeout));
}
// count - 1
public void countDown() {
sync.releaseShared(1);
}
// 獲取當前還有多少count
public long getCount() {
return sync.getCount();
}
public String toString() {
return super.toString() + "[Count = " + sync.getCount() + "]";
}
}
public class Main {
// 定義前置任務執行緒
static class PreTaskThread implements Runnable {
private final String task;
private final CountDownLatch countDownLatch;
public PreTaskThread(String task, CountDownLatch countDownLatch) {
this.task = task;
this.countDownLatch = countDownLatch;
}
@Override
public void run() {
try {
Random random = new Random();
Thread.sleep(random.nextInt(1000));
System.out.println(task + " - 任務完成");
countDownLatch.countDown();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
// 假設有三個模組需要載入
CountDownLatch countDownLatch = new CountDownLatch(3);
// 主任務
new Thread(() -> {
try {
System.out.println("等待資料載入...");
System.out.printf("還有%d個前置任務%n", countDownLatch.getCount());
countDownLatch.await();
System.out.println("資料載入完成,正式開始遊戲!");
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
// 前置任務
new Thread(new PreTaskThread("載入地圖資料", countDownLatch)).start();
new Thread(new PreTaskThread("載入人物模型", countDownLatch)).start();
new Thread(new PreTaskThread("載入背景音樂", countDownLatch)).start();
}
}
CyclicBarrier
CyclicBarrirer 從名字上來理解是“迴圈屏障”的意思。前面提到了 CountDownLatch 一旦計數值count
被降為 0 後,就不能再重新設定了,它只能起一次“屏障”的作用。而 CyclicBarrier 擁有 CountDownLatch 的所有功能,還可以使用reset()
方法重置屏障。
如果參與者(執行緒)在等待的過程中,Barrier 被破壞,就會丟擲 BrokenBarrierException。可以用isBroken()
方法檢測 Barrier 是否被破壞。
- 如果有執行緒已經處於等待狀態,呼叫 reset 方法會導致已經在等待的執行緒出現 BrokenBarrierException 異常。並且由於出現了 BrokenBarrierException,將會導致始終無法等待。
- 如果在等待的過程中,執行緒被中斷,會丟擲 InterruptedException 異常,並且這個異常會傳播到其他所有的執行緒。
- 如果在執行屏障操作過程中發生異常,則該異常將傳播到當前執行緒中,其他執行緒會丟擲 BrokenBarrierException,屏障被損壞。
- 如果超出指定的等待時間,當前執行緒會丟擲 TimeoutException 異常,其他執行緒會丟擲 BrokenBarrierException 異常。
// 構造方法
public CyclicBarrier(int parties) {
this(parties, null);
}
public CyclicBarrier(int parties, Runnable barrierAction) {
// 具體實現
}
public class Main {
static class PreTaskThread implements Runnable {
private final String task;
private final CyclicBarrier cyclicBarrier;
public PreTaskThread(String task, CyclicBarrier cyclicBarrier) {
this.task = task;
this.cyclicBarrier = cyclicBarrier;
}
@Override
public void run() {
// 假設總共三個關卡
for (int i = 1; i < 4; i++) {
try {
Random random = new Random();
Thread.sleep(random.nextInt(1000));
System.out.printf("關卡%d的任務%s完成%n", i, task);
// 一旦呼叫 await 方法的執行緒數量等於構造方法中傳入的任務總量(這裡是 3),就代表達到屏障了。
cyclicBarrier.await();
} catch (InterruptedException | BrokenBarrierException e) {
e.printStackTrace();
}
}
}
}
public static void main(String[] args) {
// CyclicBarrier 允許我們在達到屏障的時候可以執行一個任務,可以在構造方法傳入一個 Runnable 型別的物件。
CyclicBarrier cyclicBarrier = new CyclicBarrier(3, () -> {
System.out.println("本關卡所有前置任務完成,開始遊戲...");
});
new Thread(new PreTaskThread("載入地圖資料", cyclicBarrier)).start();
new Thread(new PreTaskThread("載入人物模型", cyclicBarrier)).start();
new Thread(new PreTaskThread("載入背景音樂", cyclicBarrier)).start();
}
}
CyclicBarrier 內部使用的是 Lock + Condition 實現的等待/通知模式。詳情可以檢視這個方法的原始碼:
private int dowait(boolean timed, long nanos)
Phaser
Phaser 是 Java 7 中引入的一個併發同步工具,它提供了對動態數量的執行緒的同步能力,這與 CyclicBarrier 和 CountDownLatch 不同,因為它們都需要預先知道等待的執行緒數量。Phaser 是多階段的,意味著它可以同步不同階段的多個操作。
前面我們介紹了 CyclicBarrier,可以發現它在構造方法裡傳入了“任務總量”parties
之後,就不能修改這個值了,並且每次呼叫await()
方法也只能消耗一個parties
計數。但 Phaser 可以動態地調整任務總量!
Phaser 是階段性的,所以它有一個內部的階段計數器。每當我們到達一個階段的結尾時,Phaser 會自動前進到下一個階段。
名詞解釋:
- Party:Phaser 的上下文中,一個 party 可以是一個執行緒,也可以是一個任務。當我們在 Phaser 上註冊一個 party 時,Phaser 會遞增它的參與者數量。
- arrive:對應一個 party 的狀態,初始時是 unarrived,當呼叫
arriveAndAwaitAdvance()
或者arriveAndDeregister()
進入 arrive 狀態,可以透過getUnarrivedParties()
獲取當前未到達的數量。 - register:註冊一個新的 party 到 Phaser。
- deRegister:減少一個 party。
- phase:階段,當所有註冊的 party 都 arrive 之後,將會呼叫 Phaser 的
onAdvance()
方法來判斷是否要進入下一階段。
Phaser 的終止有兩種途徑,Phaser 維護的執行緒執行完畢或者onAdvance()
返回true
。
public class Main {
static class PreTaskThread implements Runnable {
private final String task;
private final Phaser phaser;
public PreTaskThread(String task, Phaser phaser) {
this.task = task;
this.phaser = phaser;
}
@Override
public void run() {
for (int i = 1; i < 4; i++) {
try {
// 從第二個關卡起,不載入新手教程
if (i >= 2 && "載入新手教程".equals(task)) {
continue;
}
Random random = new Random();
Thread.sleep(random.nextInt(1000));
System.out.printf("關卡%d,需要載入%d個模組,當前模組【%s】%n",
i, phaser.getRegisteredParties(), task);
// 從第二個關卡起,不載入新手教程
if (i == 1 && "載入新手教程".equals(task)) {
System.out.println("下次關卡移除載入【新手教程】模組");
// 移除一個模組
phaser.arriveAndDeregister();
} else {
phaser.arriveAndAwaitAdvance();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public static void main(String[] args) {
Phaser phaser = new Phaser(4) {
@Override
protected boolean onAdvance(int phase, int registeredParties) {
System.out.printf("第%d次關卡準備完成%n", phase + 1);
return phase == 3 || registeredParties == 0;
}
};
new Thread(new PreTaskThread("載入地圖資料", phaser)).start();
new Thread(new PreTaskThread("載入人物模型", phaser)).start();
new Thread(new PreTaskThread("載入背景音樂", phaser)).start();
new Thread(new PreTaskThread("載入新手教程", phaser)).start();
}
}
這裡要注意關卡 1 的輸出,在“載入新手教程”執行緒中呼叫了arriveAndDeregister()
減少一個 party 之後,後面的執行緒使用getRegisteredParties()
得到的是已經被修改後的 parties 了。但是當前這個階段(phase),仍然是需要 4 個 parties 都 arrive 才觸發屏障的。從下一個階段開始,才需要 3 個 parties 都 arrive 就觸發屏障。
Phaser 類用來控制某個階段的執行緒數量很有用,但它並不在意這個階段具體有哪些執行緒 arrive,只要達到它當前階段的 parties 值,就觸發屏障。所以我這裡的案例雖然制定了特定的執行緒(載入新手教程)來更直觀地表述 Phaser 的功能,但其實 Phaser 是沒有分辨具體是哪個執行緒的功能的,它在意的只是數量。
它內部使用了兩個基於 Fork-Join 框架的原子類輔助:
private final AtomicReference<QNode> evenQ;
private final AtomicReference<QNode> oddQ;
static final class QNode implements ForkJoinPool.ManagedBlocker {
// 實現程式碼
}