`Thread.join()` 是 Java 多線程中最直觀的同步原語:**當前線程調用目標線程的 `join()`,將阻塞直到目標線程執行結束 (正常返回或拋出未捕獲異常)**。本文深度解析 `join()` 原理、重載版本、超時機制、與 `wait/notify` 的關係,並給出生產級使用模式。
| 方法 | 行為 | 阻塞條件 | 返回條件 |
|---|---|---|---|
| `join()` | 無限期等待 | 目標線程存活 (`isAlive()`) | 目標線程終止 |
| `join(long millis)` | 最多等待毫秒數 | 目標線程存活 且 未超時 | 目標線程終止 或 超時 |
| `join(long millis, int nanos)` | 毫秒 + 納秒精度 | 同上 | 同上 (納秒補充) |
public class JoinDemo {
private static int result = 0;
public static void main(String[] args) throws InterruptedException {
Thread worker = new Thread(() -> {
int sum = 0;
for (int i = 1; i <= 100; i++) sum += i;
result = sum; // 寫入共享變數 (需 volatile 或同步)
System.out.println("子線程計算完成: " + sum);
});
worker.start();
System.out.println("主線程等待子線程...");
worker.join(); // 阻塞在此,直到 worker 結束
System.out.println("主線程獲取結果: " + result); // 保證可見
}
}
輸出順序固定: 子線程計算完成 → 主線程獲取結果。`join()` 建立 **happens-before** 關係:子線程寫入 `result` 對主線程 `join()` 返回後可見。
JDK 源碼 (簡化):
public final void join() throws InterruptedException {
join(0); // 0 表示無限等待
}
public final synchronized void join(long millis) throws InterruptedException {
long base = System.currentTimeMillis();
long now = 0;
if (millis < 0) throw new IllegalArgumentException();
if (millis == 0) {
while (isAlive()) wait(0); // 釋放鎖,無限等待
} else {
while (isAlive()) {
long delay = millis - now;
if (delay <= 0) break;
wait(delay); // 釋放鎖,定時等待
now = System.currentTimeMillis() - base;
}
}
}
關鍵點:
thread.start();
thread.join(5000); // 最多等 5 秒
if (thread.isAlive()) {
System.out.println("子線程超時未完成,決定:繼續等待 / 記錄日誌 / interrupt() 強制中斷");
// thread.interrupt(); // 可選:嘗試中斷
} else {
System.out.println("子線程正常完成");
}
List workers = List.of(
new Thread(() -> initDatabase()),
new Thread(() -> initCache()),
new Thread(() -> initMessageQueue())
);
workers.forEach(Thread::start);
// 等待所有完成,單個超時不影響其他
for (Thread t : workers) {
try { t.join(10000); }
catch (InterruptedException e) { Thread.currentThread().interrupt(); break; }
}
System.out.println("所有服務初始化完成");
class GracefulWorker extends Thread {
private final BlockingQueue queue = new LinkedBlockingQueue<>();
private volatile boolean shutdown = false;
public void submit(Task task) { queue.offer(task); }
public void shutdown() { shutdown = true; }
@Override public void run() {
while (!shutdown || !queue.isEmpty()) {
try {
Task task = queue.poll(1, TimeUnit.SECONDS); // 可中斷等待
if (task != null) task.execute();
} catch (InterruptedException e) {
Thread.currentThread().interrupt(); break;
}
}
}
}
// 主線程關閉流程
worker.shutdown(); // 停止接受新任務
worker.join(30000); // 最多等 30 秒處理完積壓
if (worker.isAlive()) {
worker.interrupt(); // 強制中斷殘留阻塞
worker.join(5000); // 最後等待
}
// 現代代碼優先用 Future.get() 替代 join()
ExecutorService exec = Executors.newFixedThreadPool(3);
List> futures = tasks.stream()
.map(task -> exec.submit(() -> task.compute()))
.toList();
for (Future f : futures) {
try { Result r = f.get(10, TimeUnit.SECONDS); } // 帶超時獲取結果
catch (TimeoutException e) { f.cancel(true); } // 超時取消 (interrupt)
}
exec.shutdown();
| 誤區 | 真相 | 後果 |
|---|---|---|
| `join()` 會釋放當前持有的其他鎖 | 只釋放 Thread 對象鎖 (join 同步鎖),其他 `synchronized` 鎖**不釋放** | 若主線程持有業務鎖調用 join,可能導致死鎖 |
| 對已終止線程調用 `join()` 會阻塞 | 立即返回,不阻塞 | 無負面影響,但邏輯冗餘 |
| 對未啟動線程調用 `join()` | 立即返回 (isAlive()=false) | 邏輯錯誤:以為等待了執行 |
| 主線程 `join()` 子線程,子線程 `join()` 主線程 | **死鎖** (雙向等待) | 程序掛起 |
| 在 `synchronized(this)` 中調用 `thread.join()` | 持有 `this` 鎖等待 `thread` 鎖,若 `thread` 也需 `this` 鎖 → 死鎖 | 避免嵌套鎖調用 join |
| 機制 | 適用場景 | 優點 | 缺點 |
|---|---|---|---|
| `Thread.join()` | 簡單等待單個線程結束 | API 直觀、無額外對象 | 無法傳遞結果、無法中斷等待 (無超時版) |
| `wait/notify` | 複雜條件同步 | 靈活控制 | 易錯、需手動管理鎖 |
| `CountDownLatch` | 一個線程等待多個線程完成 | 計數器語義清晰、可複用 | 一次性、不可重置 |
| `Future.get()` | 需要獲取線程返回值/異常 | 結果傳遞、取消支援、超時控制 | 需線程池、稍重 |
| `CompletableFuture` | 異步流水線、組合 | 函數式、非阻塞組合 | 學習曲線陡 |
`Thread.join()` 是理解線程同步的**入門基石**,生產代碼中簡單場景可用,複雜場景建議遷移至 `Future`/`CompletableFuture`/`CountDownLatch` 等高級同步工具。核心記住:**`join()` 本質是 `synchronized(this) + wait()`,持有 Thread 對象鎖,避免嵌套鎖死鎖。**