前言:多線程方法的分類與核心契約
Java 多線程 API 分為線程生命周期控制、同步互斥、線程協作三大類。掌握每個方法的語義契約、鎖行為、異常處理、典型誤區,是寫出正確並發代碼的前提。
一、線程啟動:run() vs start()——最經典的面試陷阱
| 方法 | 語義 | 執行線程 | 是否異步 | 可重複調用 |
run() | 普通方法調用,執行任務邏輯 | 當前線程 (主線程) | 同步阻塞 | 可重複 |
start() | 啟動新線程,JVM 異步調用 run() | 新創建的線程 | 異步並發 | 不可重複 (IllegalThreadStateException) |
// 錯誤:直接調用 run(),單線程順序執行
thread.run();
// 正確:調用 start(),真正並發
thread.start();
核心契約
run() 必須是 public void run()
- 線程啟動後進入 Runnable (就緒) 狀態,由調度器決定何時運行
- 同一 Thread 實例 只能 start() 一次
二、同步互斥:synchronized 關鍵字——鎖的核心機制
三種鎖定範圍
// 1. 同步實例方法:鎖對象 = this
public synchronized void instanceMethod() { ... }
// 2. 同步靜態方法:鎖對象 = Class 對象 (如 MyClass.class)
public static synchronized void staticMethod() { ... }
// 3. 同步代碼塊:鎖對象 = 任意對象 (推薦精細控制)
public void blockMethod() {
synchronized (lockObject) { // lockObject 必須非 null
// 臨界區
}
}
經典錯誤:鎖對象不一致導致同步失效
// 錯誤示範:兩個線程持有不同鎖對象
Runnable r1 = new Task(); // 對象 A
Runnable r2 = new Task(); // 對象 B
new Thread(r1).start();
new Thread(r2).start();
// Task.run() 加了 synchronized,但鎖的是各自的 this,互不干擾!
// 正確:共享同一鎖對象
Runnable sharedTask = new Task(); // 單一對象
new Thread(sharedTask).start();
new Thread(sharedTask).start(); // 共用同一把鎖
鎖的重入性
同一線程可重複獲得同一把鎖 (計數器 +1),避免死鎖。釋放時計數器 -1,歸零才真正釋放。
三、線程休眠與讓步:sleep() vs yield()
| 特性 | sleep(millis) | yield() |
| 釋放鎖 | 不釋放 (持有 synchronized 鎖時睡眠會阻塞其他線程) | 不釋放 |
| 時間控制 | 指定毫秒/納秒 | 不可控,僅提示調度器 |
| 目標線程 | 當前線程 | 當前線程 |
| 優先級影響 | 無關 | 僅讓同優先級線程有機會 |
| 異常 | InterruptedException (必須捕獲) | 無 |
| 典型用途 | 定時任務、限流、模擬耗時 | 極少用,測試/調試佔位 |
// sleep 正確姿勢:捕獲中斷異常,保留中斷狀態
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt(); // 重置中斷標誌
// 處理中斷邏輯
}
四、線程等待:join()——等待另一線程終止
| 方法 | 語義 | 釋放鎖 | 異常 |
join() | 當前線程阻塞,直到目標線程終止 | 不釋放 | InterruptedException |
join(millis) | 帶超時的等待,超時返回不拋異常 | 不釋放 | InterruptedException |
Thread t = new Thread(() -> { /* 耗時任務 */ });
t.start();
// 主線程做其他事...
t.join(); // 這裡阻塞等待 t 結束
// t 已終止,繼續執行
應用場景
- 主線程聚合多線程計算結果
- 啟動順序有依賴的服務初始化
- 測試代碼等待異步操作完成
五、線程協作核心:wait/notify/notifyAll——Object 監視器方法
核心前提
必須在 synchronized 同步塊/方法內調用,否則拋 IllegalMonitorStateException。
狀態轉換模型
[運行中] ──wait()──► [等待集] ──notify()──► [鎖池] ──獲取鎖──► [運行中]
▲ │
│ └─notifyAll()──► [鎖池] (所有等待線程)
│
└─超時/被中斷
方法語義對比
| 方法 | 動作 | 釋放鎖 | 喚醒數量 | 喚醒後狀態 |
wait() | 當前線程進入對象等待集 | 完全釋放 | — | 等待喚醒 |
wait(timeout) | 帶超時等待 | 完全釋放 | — | 超時自動進入鎖池 |
notify() | 隨機喚醒一個等待線程 | 不釋放 (調用者持有鎖) | 1 | 進入鎖池競爭鎖 |
notifyAll() | 喚醒所有等待線程 | 不釋放 | 全部 | 進入鎖池競爭鎖 |
經典生產者-消費者模式
class Buffer {
private final List queue = new ArrayList<>();
private final int capacity = 10;
public synchronized void produce(int value) throws InterruptedException {
while (queue.size() == capacity) {
wait(); // 滿了等待,釋放鎖
}
queue.add(value);
notifyAll(); // 喚醒消費者
}
public synchronized int consume() throws InterruptedException {
while (queue.isEmpty()) {
wait(); // 空了等待,釋放鎖
}
int val = queue.remove(0);
notifyAll(); // 喚醒生產者
return val;
}
}
關鍵最佳實踐
- 永遠用 while 循環檢查條件,防止虛假喚醒 (spurious wakeup)
- 優先 notifyAll(),除非確定單一條件變量且所有等待者邏輯相同
- 釋放鎖前更新共享狀態,喚醒後重新檢查條件
- 避免在同步塊中調用耗時操作,縮小臨界區
六、高層抽象替代方案 (JDK 5+ 推薦)
| 底層 API | 高層替代 (java.util.concurrent) | 優勢 |
| synchronized | ReentrantLock, ReadWriteLock, StampedLock | 可中斷、公平鎖、條件變量、讀寫分離 |
| wait/notify | Condition (Lock.newCondition()) | 多條件隊列、精確喚醒、API 更清晰 |
| Thread + Runnable | ExecutorService, CompletableFuture, ThreadPoolExecutor | 線程池管理、異步組合、異常傳播、資源控制 |
| volatile/原子操作 | AtomicInteger, LongAdder, AtomicReference | 無鎖 CAS、高並發計數器、復合操作 |
| join | Future.get(), CompletableFuture.join() | 帶超時、異常處理、組合編排 |
七、避坑總結速查表
| 陷阱 | 後果 | 正確姿勢 |
|------|------|----------|
| 直接調用 `run()` | 偽並發,單線程執行 | 必須調用 `start()` |
| `synchronized` 鎖不同對象 | 同步失效,數據競爭 | 共享同一鎖對象 (`this`/靜態類/顯式對象) |
| `sleep()` 持有鎖睡覺 | 降低並發度,甚至死鎖 | 睡眠前釋放鎖,或用 `LockSupport.parkNanos()` |
| `wait()` 不在 `synchronized` 中 | `IllegalMonitorStateException` | 必須在同步塊內調用 |
| `notify()` 唤醒錯誤線程 | 業務邏輯錯誤、活鎖 | 條件變量用 `while` 循環檢查,優先 `notifyAll()` |
| 忽略 `InterruptedException` | 中斷信號丟失,線程無法優雅停止 | 捕獲後 `Thread.currentThread().interrupt()` 保留狀態 |
| 用 `yield()` 控制調度 | 不可靠,跨平台行為差異大 | 避免使用,改用優先級/線程池/鎖 |
總結
Java 多線程基礎 API 雖然底層,但理解其鎖語義、狀態機、協作契約是掌握 JUC 高級組件的基石。實際開發中:優先用線程池+CompletableFuture 處理異步任務,用 ReentrantLock+Condition 處理複雜同步,用原子類處理計數器。只有在極簡單場景或維護舊代碼時,才直接使用本文所述的基礎方法。