對於 Java 開發者而言,多線程是必須精通的核心技能,尤其是在開發基於 Java 的產品時。本系列文章將以「深入淺出」的方式,帶領讀者攻克 Java 多線程的關鍵知識點。後續內容將重點剖析 Java 5 中 Doug Lea 教授主導的 java.util.concurrent 並行包的設計哲學與實戰應用。
所謂「深入淺出」,關鍵在於「帶著問題學習」而非泛泛而讀。本系列將以解決實際開發問題為導向,例如線程池如何實現、鎖機制如何應用等。基礎概念(如什麼是線程、如何創建線程)請自行查閱文檔,此處不再贅述。
簡單來說,join() 用於線程同步。但具體如何同步?底層又是如何實現的?我們逐一拆解。
查閱 JDK 文檔:
public final void join(long millis) throws InterruptedException
// Waits at most millis milliseconds for this thread to die.
// A timeout of 0 means to wait forever.
字面意思是「等待該線程終止,最多等待 millis 毫秒;0 代表永久等待」。但這裡的「該線程」指的是誰?是調用 join() 的線程,還是被調用的線程對象?我們通過代碼驗證:
public class JoinTest {
public static void main(String[] args) {
Thread t = new Thread(new RunnableImpl());
t.start();
try {
t.join(1000); // 主線程最多等待 1 秒
System.out.println("joinFinish");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
class RunnableImpl implements Runnable {
@Override
public void run() {
try {
System.out.println("Begin sleep");
Thread.sleep(1000);
System.out.println("End sleep");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
輸出結果:
Begin sleep
End sleep
joinFinish
說明:主線程調用 t.join(1000) 後阻塞,等待線程 t 結束。因為 t 只睡眠 1 秒,所以主線程在 1 秒後繼續執行。
若子線程睡眠 2000 毫秒,而主線程只等待 1000 毫秒:
// 修改 RunnableImpl.run() 中的睡眠時間為 2000ms
Thread.sleep(2000);
輸出結果:
Begin sleep
joinFinish
End sleep
主線程等待 1 秒後超時返回,不再管子線程是否結束,直接打印 joinFinish。子線程繼續在後台運行,2 秒後打印 End sleep。
public final void join() throws InterruptedException { join(0); }
JDK 說明:timeout of 0 means to wait forever。實際行為是等待直到目標線程終止,而非字面上的「永遠阻塞」。
查看 JDK 源碼(精簡版):
public final synchronized void join(long millis) throws InterruptedException {
long base = System.currentTimeMillis();
long now = 0;
if (millis < 0) throw new IllegalArgumentException("timeout value is negative");
if (millis == 0) {
while (isAlive()) wait(0); // 永久等待,直到線程結束
} else {
while (isAlive()) {
long delay = millis - now;
if (delay <= 0) break;
wait(delay); // 定時等待
now = System.currentTimeMillis() - base;
}
}
}
關鍵點總結:
join() 本質是調用 Object.wait(),因此必須持有目標線程對象的鎖(即 synchronized(this))。notifyAll() 喚醒等待的線程。join() 無法進入 wait() 狀態,從而影響等待精度。public class JoinTest {
public static void main(String[] args) {
Thread t = new Thread(new RunnableImpl());
new ThreadTest(t).start(); // 佔用 t 的鎖 9 秒
t.start();
try {
t.join(); // 必須等待鎖釋放後才能 wait()
System.out.println("joinFinish");
} catch (InterruptedException e) { e.printStackTrace(); }
}
}
class ThreadTest extends Thread {
Thread thread;
public ThreadTest(Thread thread) { this.thread = thread; }
@Override public void run() { holdThreadLock(); }
public void holdThreadLock() {
synchronized (thread) { // 持有 t 的鎖
System.out.println("getObjectLock");
try { Thread.sleep(9000); } catch (InterruptedException ex) { ex.printStackTrace(); }
System.out.println("ReleaseObjectLock");
}
}
}
輸出結果:
getObjectLock
Begin sleep
End sleep
ReleaseObjectLock
joinFinish
主線程雖然調用 t.join(),但必須等待 ThreadTest 釋放鎖(9 秒後)才能進入 wait(),實際等待時間遠超預期。
join() 通過 synchronized(this) + wait()/notifyAll() 實現線程同步。使用時需注意:
t.join() 會競爭 t 對象的鎖,避免在業務代碼中長時間 synchronized(t)。join(0) 等同於 join(),均為等待線程終止,非無限期阻塞。millis 是「最長等待時間」,而非「精確等待時間」。掌握底層原理,才能在高併發場景下正確使用 join(),避免隱性 Bug。