1. CountDownLatch: 信號一起跑
在多執行緒的世界裡,經常需要讓一組執行緒協同作業——一起開始、一起結束,或一起進入下一個階段。例如:
想像一場賽車。車輛在起跑線上——有人已經暖好引擎,有人還在檢查輪胎。但在裁判揮旗前,誰也不會出發。這就是協調的任務。
或另一個例子:你和朋友一起準備晚餐——有人切菜、有人燒水、有人在找鹽跑哪去了。重點是,大家都完成準備之後,再開始烹調。
對此,Java 給了我們現成的同步工具——安全、好懂,而且不用承受 wait() 和 notify() 的折磨。其中最實用之一就是 CountDownLatch。它像是一個計數門閂:在計數歸零前,「門」是關著的,誰都不會往下走;當大家都報到之後——latch 打開,執行緒同步衝刺。
CountDownLatch
CountDownLatch 是一個「一次性閥門」,允許一個或多個執行緒等待,直到其他執行緒完成了指定次數的操作。
就像馬拉松起跑:所有跑者站在起跑線上,等待起跑槍。一聲槍響(countdown 歸零)——大家同時開跑。
到底怎麼運作
CountDownLatch 就像執行緒的起跑哨。建立時你指定一個數字——例如 3。這就像必須收到三個信號,賽事才會開始。
需要等待起跑的執行緒呼叫 await()。它們站在起跑線上,準備就緒,但還踩著煞車。其他進行準備的執行緒,在準備好時呼叫 countDown()——就像在說:「我準備好了!」。
當計數歸零——砰!——所有等待的執行緒同時出發。
但請記住:CountDownLatch 是一次性的。計數歸零後無法回復。它不是左輪手槍,而是鞭炮:炸完——就沒了。
範例:等待 N 個任務完成
import java.util.concurrent.CountDownLatch;
public class LatchDemo {
public static void main(String[] args) throws InterruptedException {
int workers = 3;
CountDownLatch latch = new CountDownLatch(workers);
for (int i = 1; i <= workers; i++) {
int id = i;
new Thread(() -> {
System.out.println("工作者 " + id + " 開始工作");
try { Thread.sleep(500 + id * 200); } catch (InterruptedException ignored) {}
System.out.println("工作者 " + id + " 結束工作");
latch.countDown(); // 遞減計數器
}).start();
}
System.out.println("主執行緒正在等待所有工作者完成...");
latch.await(); // 等待所有工作者結束
System.out.println("所有工作者都完成了!繼續主要工作。");
}
}
輸出:
主執行緒正在等待所有工作者完成...
工作者 1 開始工作
工作者 2 開始工作
工作者 3 開始工作
工作者 1 結束工作
工作者 2 結束工作
工作者 3 結束工作
所有工作者都完成了!繼續主要工作。
範例:按「信號」同時起跑
CountDownLatch startSignal = new CountDownLatch(1);
for (int i = 0; i < 5; i++) {
new Thread(() -> {
try {
System.out.println(Thread.currentThread().getName() + " 正在等待起跑");
startSignal.await(); // 等待信號
System.out.println(Thread.currentThread().getName() + " 起跑!");
} catch (InterruptedException ignored) {}
}).start();
}
Thread.sleep(1000);
System.out.println("起跑信號!");
startSignal.countDown(); // 所有執行緒同時起跑
2. CyclicBarrier: 多次階段、屏障動作
CyclicBarrier:在營火旁集合
CyclicBarrier 是執行緒的集合點。每個執行緒跑自己的路、做自己的事,然後大家在「屏障」前集合——好比在山中的營火旁。等人到齊,屏障就打開,全隊一起前進。
與 CountDownLatch 的主要差異在於:這個屏障可以反覆使用。每次集合之後,它會「重新裝填」,隊伍可以繼續前往下一個階段。
想像一下:登山隊走長程路線。每個人有自己的節奏:有人拍蝴蝶、有人找 Wi‑Fi。但在每個山口,他們都會在營火旁集合、互相等待、決定下一步。這就是 CyclicBarrier 的運作方式。
怎麼運作
你建立一個屏障並指定需要集合的參與者數量,例如 4。每個執行緒到達檢查點時呼叫 await()——然後等待其他人。當四個人都到齊,屏障「喀噠」一聲打開,所有人一起被放行。
你甚至可以指定一個「屏障動作」——當大家集合時,會恰好執行一次的一小段程式碼。例如,升起那堆營火或寫一條日誌:「階段完成,繼續前進」。為此可在建構子中傳入 Runnable。
重要:不同於一次性的 CountDownLatch,CyclicBarrier 是可重複使用的。每次「集合」之後,它都會再次就緒,準備下一個階段——就像能一次次重新點燃的營火。
範例:同步階段
import java.util.concurrent.CyclicBarrier;
public class BarrierDemo {
public static void main(String[] args) {
int parties = 3;
CyclicBarrier barrier = new CyclicBarrier(parties, () -> {
System.out.println("所有人都到達屏障!開始新階段。");
});
for (int i = 1; i <= parties; i++) {
int id = i;
new Thread(() -> {
try {
System.out.println("執行緒 " + id + " 在第 1 階段工作");
Thread.sleep(300 + id * 200);
System.out.println("執行緒 " + id + " 等待屏障");
barrier.await(); // 等待其他人
System.out.println("執行緒 " + id + " 在第 2 階段工作");
Thread.sleep(200 + id * 100);
System.out.println("執行緒 " + id + " 等待屏障 (2)");
barrier.await(); // 再次等待
System.out.println("執行緒 " + id + " 結束工作");
} catch (Exception e) {
System.out.println("錯誤:" + e);
}
}).start();
}
}
}
輸出:
執行緒 1 在第 1 階段工作
執行緒 2 在第 1 階段工作
執行緒 3 在第 1 階段工作
執行緒 1 等待屏障
執行緒 2 等待屏障
執行緒 3 等待屏障
所有人都到達屏障!開始新階段。
執行緒 1 在第 2 階段工作
...
屏障動作
可以在 CyclicBarrier 的建構子中傳入一個動作(Runnable),當所有執行緒到達屏障時,該動作會執行一次(例如更新狀態或輸出日誌)。
陷阱:如果有一個執行緒掛了?
如果某個執行緒丟出了例外或沒走到屏障,其他人會一直等待——或得到 BrokenBarrierException。屏障會「損壞」,需要重新建立。
以下這段可以改寫得更生動、有畫面感、語氣更口語——讓它自然延續前面的「樂團」主線:
3. Phaser: 大型音樂會的能幹指揮
Phaser 有點像「超級屏障」。它結合了 CountDownLatch 與 CyclicBarrier 的優點,而且更靈活。就像一個樂團:音樂家可以在不同樂章間進出,指揮仍能確保每一個樂章都在所有人準備好時開始。
不同於一般屏障,Phaser 能以階段運作——一個階段接著一個階段。有人只演第一樂章,有人晚點加入,有人提前退場——Phaser 都能淡定應對。
怎麼運作
先建立一個 Phaser,通常指定參與者數量——parties。每個執行緒註冊(register())、演奏自己的段落,並在階段末尾呼叫 arriveAndAwaitAdvance()——表示已完成並等待其他人。當大家都到達時,Phaser 會切換到下一階段,流程重複。
若某位參與者之後不再需要——可以優雅地下台,透過 arriveAndDeregister()。新的參與者也能在「演出進行中」加入——透過 register()。
何時用 Phaser 比 Barrier 好
Phaser 適合你的程式在多種節奏下運作時:
- 執行緒數量會動態變化,
- 有多個階段,且不是每位參與者都必須參加所有階段,
- 或你只想要最高的彈性,而不想辛苦做手動同步。
本質上,Phaser 就是指揮。他不只揮棒,還能依樂團編制調整、依樂章數量變化,甚至有人遲到或提前離席也能掌控全局。
範例:具有動態執行緒數的分階段處理
import java.util.concurrent.Phaser;
public class PhaserDemo {
public static void main(String[] args) {
Phaser phaser = new Phaser(1); // 主執行緒
for (int i = 1; i <= 3; i++) {
phaser.register(); // 註冊參與者
int id = i;
new Thread(() -> {
for (int phase = 1; phase <= 2; phase++) {
System.out.println("執行緒 " + id + " 在第 " + phase + " 階段工作");
try { Thread.sleep(200 + id * 100); } catch (InterruptedException ignored) {}
phaser.arriveAndAwaitAdvance(); // 等待其他人
}
System.out.println("執行緒 " + id + " 結束工作");
phaser.arriveAndDeregister(); // 從 phaser 註銷退出
}).start();
}
// 主執行緒也參與階段
for (int phase = 1; phase <= 2; phase++) {
phaser.arriveAndAwaitAdvance();
System.out.println("主執行緒:第 " + phase + " 階段已完成");
}
phaser.arriveAndDeregister();
System.out.println("所有階段都已完成!");
}
}
特點:
- 可動態新增/移除參與者。
- 可取得目前階段編號:phaser.getPhase()。
- 可強制終止 phaser:phaser.forceTermination()。
4. Exchanger: 執行緒之間成對交換資料
Exchanger<T> 是用於兩個執行緒之間交換資料的同步器。每個執行緒呼叫 exchange(data),當兩邊都到達時,彼此交換各自的資料。
比喻:兩個快遞員在十字路口碰面,交換包裹。
怎麼運作?
- 第一個執行緒呼叫 exchange(data1)——等待第二個。
- 第二個執行緒呼叫 exchange(data2)——雙方各自拿到對方的資料。
- 若第二個執行緒沒來——第一個就等待(可設定逾時)。
範例:在 producer 與 consumer 之間交換緩衝區
import java.util.concurrent.Exchanger;
public class ExchangerDemo {
public static void main(String[] args) {
Exchanger<String> exchanger = new Exchanger<>();
// Producer
new Thread(() -> {
String data = "來自 producer 的資料";
try {
System.out.println("Producer: 傳送資料");
String response = exchanger.exchange(data);
System.out.println("Producer: 收到回覆: " + response);
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
// Consumer
new Thread(() -> {
try {
String received = exchanger.exchange("來自 consumer 的回覆");
System.out.println("Consumer: 收到資料: " + received);
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
}
}
輸出:
Producer: 傳送資料
Consumer: 收到資料: 來自 producer 的資料
Producer: 收到回覆: 來自 consumer 的回覆
應用:
- 在執行緒間交換緩衝區(例如一邊從檔案讀取,另一邊往網路寫出)。
- 在兩個執行緒間同步階段。
5. 實作:平行管線處理
任務:遊戲「tick」(階段)
假設我們有多個執行緒,各自負責遊戲世界的一部分(例如物理、AI、渲染)。大家必須在每個「tick」(階段)上同步,以避免不同步。
解法:使用 CyclicBarrier 或 Phaser。
import java.util.concurrent.CyclicBarrier;
public class GameTickDemo {
public static void main(String[] args) {
int subsystems = 3;
CyclicBarrier barrier = new CyclicBarrier(subsystems, () -> {
System.out.println("所有子系統完成本次 tick。開始下一次。");
});
for (int i = 1; i <= subsystems; i++) {
int id = i;
new Thread(() -> {
for (int tick = 1; tick <= 5; tick++) {
System.out.println("子系統 " + id + " 在第 " + tick + " 次 tick 中工作");
try { Thread.sleep(100 + id * 50); } catch (InterruptedException ignored) {}
try {
barrier.await();
} catch (Exception e) {
e.printStackTrace();
}
}
}).start();
}
}
}
任務:大量 worker 的「閥門」
假設我們有 100 個 worker 執行緒,需在完成準備後同時起跑(例如負載測試)。
解法:使用 CountDownLatch。
import java.util.concurrent.CountDownLatch;
public class MassStartDemo {
public static void main(String[] args) throws InterruptedException {
int workers = 100;
CountDownLatch ready = new CountDownLatch(workers);
CountDownLatch start = new CountDownLatch(1);
for (int i = 0; i < workers; i++) {
new Thread(() -> {
System.out.println("執行緒已就緒");
ready.countDown(); // 發出就緒訊號
try {
start.await(); // 等待總體信號
System.out.println("執行緒起跑!");
} catch (InterruptedException ignored) {}
}).start();
}
ready.await(); // 等待所有執行緒準備就緒
System.out.println("全員就緒!開始!");
start.countDown(); // 發出起跑信號
}
}
6. 使用同步器時的常見錯誤
錯誤 №1:把 CountDownLatch 當作可重複使用的屏障。
CountDownLatch 是一次性的!到達零之後不能「重新裝填」。若需可重複的階段,請使用 CyclicBarrier 或 Phaser。
錯誤 №2:未處理例外(InterruptedException、BrokenBarrierException)。
await() 方法可能拋出例外——務必處理,否則執行緒可能「卡住」或以錯誤結束。請留意 InterruptedException 與 BrokenBarrierException。
錯誤 №3:某個執行緒沒走到屏障。
若有執行緒「掛了」或未呼叫 await(),其他人會一直等待(或收到 BrokenBarrierException)。請確保所有參與者都能到達屏障。
錯誤 №4:在 Phaser 中忘記 deregister()。
若執行緒已結束,但未呼叫 arriveAndDeregister(),Phaser 會一直等待「死掉」的參與者。請務必正確地將執行緒從 Phaser 中移除。
錯誤 №5:將 Exchanger 用於兩個以上的執行緒。
Exchanger 只適用於在兩個執行緒間交換資料。若超過兩個——會導致死鎖。
錯誤 №6:不了解其原理就混用不同的同步器。
不要同時對同一組執行緒使用多種屏障/閉鎖——這可能導致混亂與卡死。
GO TO FULL VERSION