應用場景
主線程需要等待一組初始化線程(數據源加載、網絡連接建立、配置拉取)全部完成,才能繼續業務邏輯。
基礎用法
Thread t1 = new Thread(() -> { loadDataSources(); }, "DataSource-Loader");
Thread t2 = new Thread(() -> { initConnections(); }, "Connection-Initializer");
t1.start();
t2.start();
try {
t1.join(); // 等待 t1 結束
t2.join(); // 等待 t2 結束
} catch (InterruptedException e) {
Thread.currentThread().interrupt(); // 保留中斷狀態
log.error("等待初始化線程被中斷", e);
}
System.out.println("所有初始化完成,啟動業務服務...");
重載版本與超時控制
| 方法 | 行為 | 典型用途 |
join() | 無限等待 | 核心初始化、必須成功 |
join(long millis) | 最多等待毫秒數 | 非核心初始化、設定啟動超時 |
join(long millis, int nanos) | 納秒級補充 | 極高精度場景(極少用) |
超時等待模式
const long STARTUP_TIMEOUT_MS = 30_000;
long start = System.currentTimeMillis();
for (Thread t : initThreads) {
long remaining = STARTUP_TIMEOUT_MS - (System.currentTimeMillis() - start);
if (remaining <= 0) throw new IllegalStateException("啟動超時");
t.join(remaining);
}
原理剖析
// Thread.join() 簡化源碼 (JDK 17)
public final void join() throws InterruptedException {
join(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); // 關鍵:wait() 釋放鎖,進入 WAITING
} else {
while (isAlive()) {
long delay = millis - now;
if (delay <= 0) break;
wait(delay);
now = System.currentTimeMillis() - base;
}
}
}
本質:調用者持有線程對象鎖,調用 wait() 進入等待集合;目標線程終止時 JVM 自動 notify_all()。
最佳實踐
- 捕獲 InterruptedException 後恢復中斷狀態:
Thread.currentThread().interrupt()
- 避免死等:關鍵初始化用無限 join,非關鍵用超時 join
- 線程命名:便於排查堆棧、日誌關聯
- 考慮 CountDownLatch / CompletableFuture:更靈活的多線程協作工具
對比:join vs CountDownLatch
| 維度 | Thread.join() | CountDownLatch |
| 等待對象 | Thread 實例 | 計數器 |
| 靈活性 | 綁定線程生命周期 | 任意任務、可復用 |
| 中斷響應 | 拋出 InterruptedException | 同 |
| 適用場景 | 等待特定線程結束 | 等待 N 個任務完成、可分離線程 |
完整示例:啟動器模式
public class AppBootstrap {
private static final Logger log = LoggerFactory.getLogger(AppBootstrap.class);
public static void main(String[] args) {
List initThreads = List.of(
new Thread(() -> { sleep(2000); log.info("數據源就緒"); }, "DS"),
new Thread(() -> { sleep(3000); log.info("緩存預熱完成"); }, "Cache"),
new Thread(() -> { sleep(1500); log.info("配置中心拉取完成"); }, "Config")
);
initThreads.forEach(Thread::start);
try {
for (Thread t : initThreads) t.join(10_000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
log.error("啟動被中斷", e);
System.exit(1);
}
log.info("=== 應用啟動成功 ===");
// 啟動 Web 容器、gRPC 服務等
}
private static void sleep(long ms) {
try { Thread.sleep(ms); } catch (InterruptedException e) { Thread.currentThread().interrupt(); }
}
}
- 上一篇文章: Java final 修飾符深度解析:變量、方法、類三大場景與常見誤區
- 下一篇文章: Eclipse 生成 Javadoc 文檔完全指南:三種方式、亂碼解決、Maven 整合
Copyright ©
程式碼之家 All Rights Reserved