Java?Thread.join()方法使用詳細(xì)解析
前言
全面介紹Java 中Thread.join()方法的用法,包括其核心作用、重載方法、工作原理、使用場景及注意事項(xiàng),從基礎(chǔ)到進(jìn)階逐步拆解,結(jié)合可運(yùn)行示例幫你徹底理解。
一、核心定位
Thread.join() 是 Thread 類的實(shí)例方法,核心作用是實(shí)現(xiàn)線程間的同步(等待機(jī)制):當(dāng)一個線程(如主線程)調(diào)用另一個目標(biāo)線程(如子線程)的join()方法時,調(diào)用線程會進(jìn)入阻塞狀態(tài),直到目標(biāo)線程執(zhí)行完畢(進(jìn)入 TERMINATED 狀態(tài))或等待超時,才會繼續(xù)執(zhí)行自身后續(xù)代碼。
簡單說:A.join() 讓「當(dāng)前線程」等「線程 A」執(zhí)行完,再繼續(xù)自己的工作。
二、重載方法
Java 提供了 3 個重載的join()方法,滿足不同等待需求:
| 方法簽名 | 功能說明 |
|---|---|
void join() throws InterruptedException | 無參重載:無限等待,調(diào)用線程會一直阻塞,直到目標(biāo)線程完全執(zhí)行終止(不會主動超時) |
void join(long millis) throws InterruptedException | 帶毫秒?yún)?shù):超時等待,調(diào)用線程最多阻塞millis毫秒(千分之一秒),超時后自動喚醒繼續(xù)執(zhí)行 |
void join(long millis, int nanos) throws InterruptedException | 帶毫秒 + 納秒?yún)?shù):高精度超時等待,理論上支持納秒級精度,但實(shí)際受操作系統(tǒng)時鐘精度限制(一般無需使用,優(yōu)先用毫秒重載) |
關(guān)鍵說明
join(0)等價(jià)于join():表示「無限等待目標(biāo)線程終止」,并非等待 0 毫秒。- 納秒?yún)?shù)(
nanos)取值范圍:0-999999,超出該范圍會拋出IllegalArgumentException。 - 所有重載方法均會拋出
InterruptedException(受檢異常),必須捕獲或聲明拋出(因?yàn)榈却械木€程可能被其他線程中斷)。
三、工作原理
1. 核心邏輯
當(dāng)線程T1調(diào)用線程T2.join()時,底層執(zhí)行流程如下:
T1(調(diào)用線程)會先判斷T2(目標(biāo)線程)是否還處于存活狀態(tài)(isAlive());- 若
T2仍存活,T1會調(diào)用Object.wait()方法(底層依賴 Object 的等待機(jī)制),進(jìn)入阻塞狀態(tài); - 當(dāng)
T2執(zhí)行完畢終止時,JVM 會自動調(diào)用T2的notifyAll()方法,喚醒所有等待在T2對象上的線程(即T1); - 若設(shè)置了超時時間,
T1在等待超時后會自動喚醒,無需T2終止。
2. 與Thread.sleep()的核心區(qū)別
很多人會混淆join()和sleep(),二者的核心差異在于鎖釋放和使用場景:
| 特性 | Thread.join() | Thread.sleep(long millis) |
|---|---|---|
| 方法類型 | 實(shí)例方法(針對目標(biāo)線程) | 靜態(tài)方法(針對當(dāng)前線程) |
| 鎖處理 | 阻塞時會釋放當(dāng)前線程持有的對象鎖 | 阻塞時不釋放任何鎖資源 |
| 等待狀態(tài) | 無參:WAITING;帶參:TIMED_WAITING | TIMED_WAITING |
| 喚醒條件 | 目標(biāo)線程終止 / 等待超時 / 被中斷 | 時間到達(dá) / 被中斷 |
| 核心場景 | 線程間同步(等待其他線程完成) | 當(dāng)前線程暫停指定時間 |
四、實(shí)戰(zhàn)示例
示例 1:無參join()- 主線程等待子線程完全執(zhí)行
public class JoinBasicDemo {
public static void main(String[] args) {
// 定義子線程:模擬耗時任務(wù)(打印1-5,每次間隔500ms)
Thread subThread = new Thread(() -> {
for (int i = 1; i <= 5; i++) {
try {
Thread.sleep(500); // 模擬任務(wù)耗時
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("子線程執(zhí)行中:i = " + i);
}
System.out.println("子線程執(zhí)行完畢!");
}, "子線程-Test");
System.out.println("主線程啟動子線程...");
subThread.start(); // 啟動子線程
try {
System.out.println("主線程開始等待子線程執(zhí)行完畢...");
subThread.join(); // 主線程阻塞,等待subThread執(zhí)行完
} catch (InterruptedException e) {
e.printStackTrace();
}
// 子線程執(zhí)行完后,主線程才會執(zhí)行這里
System.out.println("主線程繼續(xù)執(zhí)行,程序結(jié)束!");
}
}
運(yùn)行結(jié)果(順序固定)
plaintext
主線程啟動子線程... 主線程開始等待子線程執(zhí)行完畢... 子線程執(zhí)行中:i = 1 子線程執(zhí)行中:i = 2 子線程執(zhí)行中:i = 3 子線程執(zhí)行中:i = 4 子線程執(zhí)行中:i = 5 子線程執(zhí)行完畢! 主線程繼續(xù)執(zhí)行,程序結(jié)束!
示例 2:帶超時join(long millis)- 主線程等待超時后繼續(xù)執(zhí)行
修改示例 1 中的join()調(diào)用,設(shè)置超時時間 1500ms(子線程總耗時 2500ms):
public class JoinTimeoutDemo {
public static void main(String[] args) {
Thread subThread = new Thread(() -> {
for (int i = 1; i <= 5; i++) {
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("子線程執(zhí)行中:i = " + i);
}
System.out.println("子線程執(zhí)行完畢!");
}, "子線程-Timeout");
System.out.println("主線程啟動子線程...");
subThread.start();
try {
System.out.println("主線程開始等待子線程(超時1500ms)...");
subThread.join(1500); // 最多等待1500ms
} catch (InterruptedException e) {
e.printStackTrace();
}
// 1500ms后,主線程不再等待,直接執(zhí)行后續(xù)代碼
System.out.println("主線程等待超時,繼續(xù)執(zhí)行自身邏輯!");
}
}
運(yùn)行結(jié)果(主線程超時后提前執(zhí)行)
plaintext
主線程啟動子線程... 主線程開始等待子線程(超時1500ms)... 子線程執(zhí)行中:i = 1 子線程執(zhí)行中:i = 2 子線程執(zhí)行中:i = 3 主線程等待超時,繼續(xù)執(zhí)行自身邏輯! 子線程執(zhí)行中:i = 4 子線程執(zhí)行中:i = 5 子線程執(zhí)行完畢!
示例 3:多個線程join()- 串行等待 vs 并行等待
場景 1:串行等待(主線程依次等待多個子線程,子線程串行執(zhí)行)
public class JoinSerialDemo {
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
for (int i = 1; i <= 3; i++) {
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("線程t1執(zhí)行中:i = " + i);
}
}, "線程t1");
Thread t2 = new Thread(() -> {
for (int i = 1; i <= 3; i++) {
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("線程t2執(zhí)行中:i = " + i);
}
}, "線程t2");
t1.start();
t1.join(); // 主線程先等t1執(zhí)行完
t2.start();
t2.join(); // 再等t2執(zhí)行完
System.out.println("所有子線程執(zhí)行完畢,主線程結(jié)束!");
}
}
場景 2:并行等待(主線程同時等待多個已啟動的子線程,子線程并行執(zhí)行)
public class JoinParallelDemo {
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
for (int i = 1; i <= 3; i++) {
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("線程t1執(zhí)行中:i = " + i);
}
}, "線程t1");
Thread t2 = new Thread(() -> {
for (int i = 1; i <= 3; i++) {
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("線程t2執(zhí)行中:i = " + i);
}
}, "線程t2");
// 先同時啟動兩個子線程
t1.start();
t2.start();
// 主線程同時等待兩個子線程(誰先執(zhí)行完誰先喚醒,最終等待耗時為較慢線程的執(zhí)行時間)
t1.join();
t2.join();
System.out.println("所有子線程執(zhí)行完畢,主線程結(jié)束!");
}
}
五、關(guān)鍵注意事項(xiàng)(避坑指南)
- 調(diào)用對象必須是已啟動的線程:若對未啟動(NEW 狀態(tài))的線程調(diào)用
join(),方法會直接返回,不會產(chǎn)生阻塞(因?yàn)?code>isAlive()返回 false,無需等待)。Thread t = new Thread(() -> {}); t.join(); // 無阻塞,直接執(zhí)行后續(xù)代碼(t未啟動) - 異常必須處理:
join()拋出的InterruptedException是受檢異常,若不捕獲 / 聲明拋出,編譯報(bào)錯。當(dāng)調(diào)用線程被中斷時,會觸發(fā)該異常并清除中斷狀態(tài)。 join()不改變線程的啟動狀態(tài):join()僅負(fù)責(zé)阻塞調(diào)用線程,不會自動啟動目標(biāo)線程,必須先調(diào)用targetThread.start(),再調(diào)用targetThread.join()。- 多個線程等待同一個目標(biāo)線程:若多個線程同時調(diào)用同一個目標(biāo)線程的
join()方法,當(dāng)目標(biāo)線程終止時,所有等待線程會被 JVM 同時喚醒(底層notifyAll())。 - 與鎖結(jié)合時的釋放特性:若調(diào)用線程持有某個對象鎖,調(diào)用
join()后會釋放該鎖(因?yàn)榈讓诱{(diào)用wait()),而sleep()不會釋放鎖,這是并發(fā)編程中的關(guān)鍵差異。
六、底層源碼簡化解析(幫助理解)
以下是Thread.join()的核心源碼(JDK8),剔除了冗余邏輯,保留核心流程:
// 無參join() 等價(jià)于 join(0)
public final void join() throws InterruptedException {
join(0);
}
// 帶毫秒?yún)?shù)的核心實(shí)現(xiàn)
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");
}
// millis=0 表示無限等待
if (millis == 0) {
// 只要目標(biāo)線程還存活,調(diào)用線程就一直wait
while (isAlive()) {
wait(0);
}
} else {
// 超時等待邏輯:循環(huán)判斷,避免虛假喚醒
while (isAlive()) {
long delay = millis - now;
if (delay <= 0) {
break;
}
wait(delay);
now = System.currentTimeMillis() - base;
}
}
}
源碼關(guān)鍵點(diǎn)
join()是synchronized方法:鎖對象是目標(biāo)線程實(shí)例(保證線程安全)。- 循環(huán)判斷
isAlive():避免「虛假喚醒」(即使被意外喚醒,若目標(biāo)線程未終止,仍會繼續(xù)等待)。 - 依賴
Object.wait():底層是 Object 的等待 / 喚醒機(jī)制,JVM 在目標(biāo)線程終止時自動調(diào)用notifyAll()。
七、總結(jié)
- 核心功能:
Thread.join()實(shí)現(xiàn)線程同步,讓調(diào)用線程阻塞等待目標(biāo)線程終止或超時。 - 核心重載:無參(無限等待)、
join(long millis)(毫秒超時)、join(long millis, int nanos)(高精度超時,極少使用)。 - 關(guān)鍵差異:與
sleep()的核心區(qū)別是「釋放對象鎖」和「使用場景」,join()用于線程同步,sleep()用于當(dāng)前線程暫停。 - 避坑要點(diǎn):先啟動目標(biāo)線程再調(diào)用
join()、處理InterruptedException、區(qū)分串行 / 并行等待。 - 底層邏輯:基于
Object.wait()實(shí)現(xiàn),通過循環(huán)判斷isAlive()避免虛假喚醒,目標(biāo)線程終止時由 JVM 喚醒等待線程。
到此這篇關(guān)于Java Thread.join()方法使用的文章就介紹到這了,更多相關(guān)Java Thread.join()方法內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
5分鐘快速創(chuàng)建spring boot項(xiàng)目的完整步驟
這篇文章主要給大家介紹了關(guān)于通過5分鐘快速創(chuàng)建spring boot項(xiàng)目的完整步驟,文中通過示例代碼介紹的非常詳細(xì),對大家學(xué)習(xí)或者使用spring boot具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面來一起學(xué)習(xí)學(xué)習(xí)吧2019-06-06
關(guān)于javax.validation.constraints的超詳細(xì)說明
這篇文章主要給大家介紹了關(guān)于javax.validation.constraints的超詳細(xì)說明,文中通過代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2025-07-07
Java 中的單例類(Singleton)應(yīng)用場景分析
單例類是一種設(shè)計(jì)模式,確保一個類只有一個實(shí)例,并提供一個全局訪問點(diǎn),本文給大家介紹Java中的單例類(Singleton)應(yīng)用場景分析,感興趣的朋友跟隨小編一起看看吧2025-09-09
MyBatis 多個條件使用Map傳遞參數(shù)進(jìn)行批量刪除方式
這篇文章主要介紹了MyBatis 多個條件使用Map傳遞參數(shù)進(jìn)行批量刪除方式,具有很好的參考價(jià)值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2021-12-12
Spring Cloud Feign實(shí)現(xiàn)文件上傳下載的示例代碼
Feign框架對于文件上傳消息體格式并沒有做原生支持,需要集成模塊feign-form來實(shí)現(xiàn),本文就詳細(xì)的介紹一下如何使用,感興趣的可以了解一下2022-02-02
基于Springboot+Mybatis對數(shù)據(jù)訪問層進(jìn)行單元測試的方式分享
本文將介紹一種快高效、可復(fù)用的解決測試方案——對數(shù)據(jù)訪問層做單元測試,文章通過代碼示例介紹的非常詳細(xì),具有一定的參考價(jià)值,需要的朋友可以參考下2023-07-07

