1.?線程的狀態
1. 線程狀態分類(Thread.State
?枚舉)
Java 定義了 6 種線程狀態,這些狀態均由?java.lang.Thread.State
?枚舉表示:
-
NEW(新建)
線程對象已創建,但尚未調用?start()
?方法。此時線程尚未啟動,只是一個普通的 Java 對象。 -
RUNNABLE(可運行)
線程已調用?start()
?方法,正在 JVM 中運行。該狀態包含兩種實際情況:- READY(就緒):線程已獲取除 CPU 外的所有資源,等待操作系統調度。
- RUNNING(運行中):線程正在 CPU 上執行。
-
BLOCKED(阻塞)
線程因等待監視器鎖(如進入?synchronized
?塊 / 方法)而被阻塞。線程會在獲取鎖后恢復為 RUNNABLE 狀態。 -
WAITING(無限期等待)
線程因調用以下方法而進入無限期等待狀態,必須等待其他線程顯式喚醒:Object.wait()
Thread.join()
LockSupport.park()
-
喚醒條件:
-
notify()
/notifyAll()
-
目標線程終止(針對?
join()
)
-
-
TIMED_WAITING(限期等待)
線程因調用以下帶超時參數的方法而進入限期等待狀態,超時后自動喚醒:Thread.sleep(long millis)
Object.wait(long timeout)
Thread.join(long millis)
LockSupport.parkNanos()
LockSupport.parkUntil()
-
TERMINATED(終止)
線程執行完畢(run()
?方法正常退出)或因異常終止,線程生命周期結束。
2. 線程的狀態和轉移
public class ThreadDomo1 {public static void main(String[] args) {Thread t = new Thread(()->{for(int i=0;i<1;i++){}});System.out.println(t.getState());//NEWt.start();while(t.isAlive()){//線程存活System.out.println(t.getState());//RUNNABLE}System.out.println(t.getState());//TERMINATED}
}
- 線程狀態不可逆:線程一旦進入 TERMINATED 狀態,無法再次啟動(調用?
start()
?會拋出?IllegalThreadStateException
)。 - BLOCKED 與 WAITING 的區別:
- BLOCKED 是因等待監視器鎖而阻塞。
- WAITING/TIMED_WAITING 是主動調用方法進入等待狀態,需顯式喚醒或超時。
2. 線程安全
Java 標準庫中的線程安全類
1. Java 標準庫中很多都是線程不安全的. 這些類可能會涉及到多線程修改共享數據, ?沒有任何加鎖措施.? ArrayList? LinkedList? HashMap? TreeMap? HashSet? TreeSet? StringBuilder2. 但是還有?些是線程安全的. 使?了?些鎖機制來控制.? Vector (不推薦使?)? HashTable (不推薦使?)? ConcurrentHashMap? StringBuffer3. 還有的雖然沒有加鎖, 但是不涉及 "修改", 仍然是線程安全的? String
線程安全是多線程編程中的核心概念,指的是在多線程環境下,程序的行為和結果與單線程環境下一致,不會出現數據競爭、不一致或其他意外情況。
如果這個代碼在單線程環境下運行正確,在多線程環境下產生 bug ,這種情況就稱為“線程不安全”或“存在線程安全問題”?。
public class ThreadDomo2 {public static int count = 0;public static void main(String[] args) throws InterruptedException {Thread t1 = new Thread(()->{for(int i=0;i<50000;i++){count++;}});Thread t2 = new Thread(()->{for(int i=0;i<50000;i++){count++;}});t1.start();t2.start();t1.join();t2.join();System.out.println(count);//預期結果 10w}
}
運行代碼發現,結果每次都不一樣,結果大概在5w-10w之間,這是為什么?
1. 線程不安全原因
1. 【根本原因】線程調度是隨機的
操作系統上的線程是“搶占式執行” “隨機調度”?
2.?修改共享數據
代碼結構:進程中多個線程同時修改同一個變量
? 沒有問題:1. 一個線程修改一個變量
? ? ? ? ? ? ? ? ? ? 2. 多個線程讀取同一個變量
? ? ? ? ? ? ? ? ? ? 3. 多個線程修改不同變量
3. 【直接原因】原子性
多線程同時修改同一個變量操作不是原子操作
count++; 由三個指令構成:
? 1. load? 從內存中讀取數據到 cpu 寄存器
? 2. add 把寄存器數值 +1
? 3. save 把寄存器的值寫回到 內存 中
t1 和 t2 是并發執行的,可能交錯執行這三步,導致部分增量丟失。
1,2 為線程安全,其余都為線程不安全
關鍵在于,要確保前一個線程 save 之后,第二個線程再 load ,否則第二個線程 load 到的結果就是第一個線程自增前的結果,兩次自增就只 +1
即一個線程執行?1-n(基本為1次)這自增,被另一個線程覆蓋成自增 1 次的情況。
4. 可見性

Java 內存模型 (JMM): Java虛擬機規范中定義了Java內存模型.?的是屏蔽掉各種硬件和操作系統的內存訪問差異,以實現讓Java程序在各種平臺下都能達到?致的并發效果.? 線程之間的共享變量存在 主內存 (Main Memory).? 每?個線程都有??的 "?作內存" (Working Memory) .? 當線程要讀取?個共享變量的時候, 會先把變量從主內存拷?到?作內存, 再從?作內存讀取數據.? 當線程要修改?個共享變量的時候, 也會先修改?作內存中的副本, 再同步回主內存.所謂的 "主內存" 才是真正硬件?度的 "內存". ?所謂的 "?作內存", 則是指 CPU 的寄存器和?速緩存.CPU 訪問??寄存器的速度以及?速緩存的速度, 遠遠超過訪問內存的速度(快了 3 - 4 個數量級, 也就是?千倍, 上萬倍).
import java.util.Scanner;public class ThreadDomo3 {public static int flg = 1;//使用 volatile 關鍵字//public static volatile int flg = 1;public static void main(String[] args) throws InterruptedException {Scanner scan = new Scanner(System.in);Thread t1 = new Thread(()->{while(flg==1){ // 循環檢查 flg 的值// 空循環,等待 flg 變為非 1}System.out.println("t1 線程結束");});Thread t2 = new Thread(()->{System.out.print("請輸入flg的值:");flg = scan.nextInt();// 從控制臺讀取輸入,修改 flg 的值System.out.println("t2 線程結束");});t1.start();t2.start();}
}
? ?
使用 volatile 關鍵字
在該代碼中,我們預期是通過 t2 線程輸入一個非 1 數,使 t1 線程結束,事實卻是我們輸入非 1 整數,t1 線程并未結束,這是為什么?
while(flg == 1);核心指令有兩條:
1. load 讀取內存中?flg 的值到cpu寄存器中
2. 拿寄存器中獲取的值與1進行比較(條件跳轉指令)
頻繁執行 load 操作和條件跳轉,load 操作執行的結果,每次都是一樣的,且 load 操作開銷遠遠高于條件跳轉,訪問寄存器的操作速度遠遠超過訪問內存,此時 jvm 就可能做出代碼優化,優化掉 load 操作,以提高循環的執行速度。卻導致 t2 線程對共享變量的修改無法及時被 t1 線程看到。造成線程不安全。
這種優化被稱為?循環不變代碼外提(Loop Invariant Code Motion),它將循環內不變的操作(如?load
)移到循環外,大幅提高執行效率。但在多線程環境下,這種優化會導致?內存可見性問題:即使其他線程修改了?flg
?的值,執行優化后的線程仍使用寄存器中的舊值。
volatile 關鍵字
volatile
?是一個用于修飾變量的關鍵字,主要用于保證變量的內存可見性和禁止指令重排序。
保證可見性每次訪問變量必須要重新讀取內存,而不會優化到寄存器/緩存中代碼在寫? volatile 修飾的變量的時候,? ?? 改變線程?作內存中volatile變量副本的值? ?? 將改變后的副本的值從?作內存刷新到主內存代碼在讀取 volatile 修飾的變量的時候? ?? 從主內存中讀取volatile變量的最新值到線程的?作內存中? ?? 從?作內存中讀取volatile變量的副本
禁止指令重排序
針對被 volatile 修飾的變量必須都要重新讀取內存,而不會優化到寄存器/緩存中
- 原理:
volatile
?變量會插入內存屏障(Memory Barrier),禁止編譯器和處理器對指令進行重排序
5. 指令重排序
- 編譯器重排序:編譯器為優化性能,可能改變代碼的執行順序。
- 處理器重排序:處理器為提高指令執行效率,可能對指令進行亂序執行。
2.?synchronized 關鍵字 - 監視器鎖 monitor lock
針對線程不安全原因3,使用 加鎖 的方式,將非原子指令打包成一個整體,確保同一時間,只有該線程可以執行此非原子指令。
synchronized
?關鍵字是實現線程同步的核心機制之一,它基于 ** 監視器鎖(Monitor Lock)** 來保證同一時間只有一個線程可以執行被保護的代碼塊或方法
1、監視器鎖的底層原理
-
每個對象都有一個監視器鎖
Java 中每個對象(包括類實例和數組)都關聯著一個監視器鎖(也稱為內部鎖或互斥鎖)。當一個線程試圖訪問被?synchronized
?保護的代碼時,它必須先獲取該對象的監視器鎖。 -
鎖的獲取與釋放
- 獲取鎖:線程進入?
synchronized
?代碼塊前,必須獲取對象的監視器鎖。如果鎖已被其他線程持有,則當前線程會被阻塞,進入鎖的等待隊列。 - 釋放鎖:線程退出?
synchronized
?代碼塊時,會自動釋放監視器鎖,喚醒等待隊列中的其他線程競爭鎖。
- 獲取鎖:線程進入?
- JVM 實現
監視器鎖的實現依賴于對象頭中的?Mark Word。當對象被鎖定時,Mark Word 會存儲指向鎖記錄的指針,不同狀態(偏向鎖、輕量級鎖、重量級鎖)下的存儲結構不同
2.?synchronized
?的使用方式
1.?同步實例方法(鎖對象為 this)
使用當前對象實例(this
)作為鎖
直接修飾普通?法
public class SynchronizedDomo {public synchronized void method(){//...}
}
- 鎖對象:隱式使用當前對象實例(
this
)。 - 作用范圍:整個方法體。
- 字節碼層面:JVM 使用?
ACC_SYNCHRONIZED
?標志來標記方法,當線程調用該方法時,會自動獲取鎖并在方法退出時釋放鎖。
同步代碼塊
public class SynchronizedDomo {public void method() {synchronized (this) {// 同步代碼}}
}
- 鎖對象:顯式指定為?
this
(當前對象實例)。 - 作用范圍:僅包含在?
{}
?內的代碼。 - 字節碼層面:使用?
monitorenter
?和?monitorexit
?指令實現鎖的獲取和釋放。
2.?同步靜態方法(鎖對象為類的 Class 對象)
使用類的?Class
?對象(即?SynchronizedDomo1.class
)作為鎖
synchronized
?修飾靜態方法
public class SynchronizedDomo {public static synchronized void method(){//...}
}
- 鎖對象:隱式使用當前類的?
Class
?對象(如?SynchronizedDomo1.class
)。 - 作用范圍:整個靜態方法體。
- 字節碼層面:JVM 使用?
ACC_SYNCHRONIZED
?標志來標記靜態方法,當線程調用該方法時,會自動獲取類的?Class
?對象鎖并在方法退出時釋放鎖。
同步靜態代碼塊
public class SynchronizedDomo {public static void method() {// 反射 類名.class 獲取當前類的 class 對象synchronized (SynchronizedDomo1.class) {// 同步代碼}}
}
- 鎖對象:顯式指定為當前類的?
Class
?對象。 - 作用范圍:僅包含在?
{}
?內的代碼。 - 字節碼層面:使用?
monitorenter
?和?monitorexit
?指令實現鎖的獲取和釋放。
3. 同步代碼塊,指定鎖對象(locker)
public class SynchronizedDomo1 {//創建鎖對象(鎖對象可以是任意Object)private Object locker = new Object();public void method(){synchronized (locker){//...}}
}
代碼實例
public class ThreadDomo3 {public static int count = 0; //共享變量public static void main(String[] args) throws InterruptedException {//創建對象(任意)作為鎖對象Object locker = new Object();Thread t1 = new Thread(()->{for(int i=0;i<50000;i++){// 使用locker作為鎖,進入同步塊前會獲取鎖synchronized (locker){count++;}// 退出同步塊時自動釋放鎖}});Thread t2 = new Thread(()->{for(int i=0;i<50000;i++){// 使用locker作為鎖,進入同步塊前會獲取鎖synchronized (locker){ count++;} // 退出同步塊時自動釋放鎖//count++;}});// 啟動兩個線程t1.start();t2.start();// 主線程等待兩個線程執行完畢t1.join();t2.join();System.out.println(count);//100000}
}
3. 互斥
-
互斥?指同一時間只允許一個線程訪問共享資源,其他線程必須等待。
-
通過?鎖(Lock)?或?同步機制(如?
synchronized
)實現。
作用
- 保證原子性:防止多個線程同時修改共享數據導致的數據不一致。
- 維護可見性:確保一個線程對共享變量的修改能被其他線程正確看到。
- 防止多個線程同時修改共享數據(如?
count++
),造成線程不安全
4. 鎖競爭
鎖競爭是指多個線程同時嘗試獲取同一把鎖時發生的競爭現象。當鎖被一個線程持有時,其他線程必須等待,從而導致線程阻塞和上下文切換,降低程序性能。
競爭程度 | 表現 | 解決方案 |
---|---|---|
低競爭 | 線程偶爾阻塞,性能影響小 | 無優化必要 |
高競爭 | 大量線程阻塞,CPU空轉 | 減小鎖粒度、無鎖算法 |
在上述代碼中,兩個線程訪問共享資源 count,?t1 線程
進行了同步保護,t2
?線程直接訪問,就不會形成鎖競爭,t2
?線程可能看不到?t1
?線程對 count?的修改,count ++的原子性被破壞,造成線程不安全。
? ?
??
5. 可重入
可重入是指同一個線程可以多次獲取同一把鎖而不會被阻塞。可重入鎖會記錄鎖的持有線程和重入次數,當線程退出同步塊時,只有重入次數降為 0 才會真正釋放鎖。
-
實現原理:
-
synchronized
?通過?鎖計數器?記錄重入次數。 -
ReentrantLock
?通過?getHoldCount()
?獲取重入次數。
-
Java 中的可重入鎖
synchronized
?關鍵字:隱式支持可重入。ReentrantLock
:顯式支持可重入,可通過?lock()
?和?unlock()
?方法控制。
public class SynchronizedDomo4 {public static int count = 0;public static void main(String[] args) throws InterruptedException {Object locker = new Object();Thread t = new Thread(()->{// 真正加鎖,同時記錄鎖的持有線程synchronized(locker){ // 第一次獲取locker鎖,鎖計數器為1count++;synchronized (locker){ // 第二次獲取同一個locker鎖,鎖計數器為2count++;synchronized (locker){ // 第三次獲取同一個locker鎖,鎖計數器為3count++;}//解鎖,鎖計數器為2}//解鎖,鎖計數器為1}//真正解鎖,鎖計數器為0});t.start();t.join();System.out.println(count);//3}
}
public class SynchronizedDomo5 {public void A(){synchronized (this){// 子線程獲取 this 鎖,鎖計數器+1 → 1B();}}public void B(){C();}public void C(){D();}public void D(){synchronized (this){// 子線程再次獲取 this 鎖,計數+1 → 2(已持有鎖,可重入)System.out.println("hello");}}public static void main(String[] args) throws InterruptedException {SynchronizedDomo5 s = new SynchronizedDomo5();Thread t = new Thread(()->{s.A();});t.start();}
}
雖然?A()
?和?D()
?都使用?synchronized (this)
?加鎖,但由于鎖是可重入的,同一個線程可以在持有鎖的狀態下嵌套調用其他同步方法,不會導致死鎖。
概念 | 互斥(Mutual Exclusion) | 鎖競爭(Lock Contention) | 可重入性(Reentrancy) |
---|---|---|---|
目標 | 保護共享資源 | 減少鎖沖突 | 避免自我阻塞 |
實現手段 | 鎖機制 | 鎖優化或無鎖算法 | 鎖計數器 |
關聯性 | 互斥導致鎖競爭 | 高競爭降低性能 | 可重入減少死鎖 |
關鍵點 | 同一時間只有一個線程能訪問共享資源。 | 多個線程爭奪同一把鎖,導致阻塞和上下文切換。 | 同一個線程可多次獲取同一把鎖。 |
6. 死鎖
兩個或多個線程互相持有對方需要的資源,導致所有線程都無法繼續執行的狀態。
死鎖的四個必要條件(Coffman條件)
- 互斥條件:資源不能被共享,同一時間只能被一個線程占用。
- 占有并等待:線程至少已經持有一個資源,同時請求其他線程持有的資源。
- 不可搶占:線程已獲得的資源不能被其他線程強行搶占,只能自己釋放。
-
循環等待:存在一個線程的循環等待鏈,每個線程都在等待下一個線程所占用的資源。
public class SynchronizedDomo6 {public static void main(String[] args) {Object locker1 = new Object();Object locker2 = new Object();Thread t1 = new Thread(()->{synchronized (locker1){//休眠1s,為線程2爭取獲得locker2的時間try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}//嘗試獲取locker2synchronized (locker2){System.out.println("t1");}}});Thread t2 = new Thread(()->{synchronized (locker2){//休眠1s,為線程1爭取獲得locker1的時間try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}//嘗試獲取locker1synchronized (locker1){System.out.println("t2");}}});t1.start();t2.start();}
}
?
t1
?先獲取?locker1
?鎖,然后休眠 1 秒,在這 1 秒內,t2
?有機會獲取?locker2
?鎖。t2
?先獲取?locker2
?鎖,然后休眠 1 秒,在這 1 秒內,t1
?持有?locker1
?鎖。- 當?
t1
?休眠結束后嘗試獲取?locker2
?鎖時,t2
?已持有?locker2
?鎖;而當?t2
?休眠結束后嘗試獲取?locker1
?鎖時,t1
?已持有?locker1
?鎖。 - 這樣就形成了?
t1
?等待?t2
?釋放?locker2
?鎖,t2
?等待?t1
?釋放?locker1
?鎖的情況,兩個線程相互等待對方釋放鎖,從而導致死鎖。程序卡死。
如何避免死鎖
-
破壞互斥條件:不是所有資源都能這樣做(如打印機必須互斥使用)
-
破壞占有并等待:
-
線程在開始時就獲取所有需要的鎖,否則不獲取任何鎖。
-
使用
tryLock()
等非阻塞獲取鎖的方法
-
-
破壞不可搶占條件:
-
使用可響應中斷的鎖(如
ReentrantLock
) -
設置獲取鎖的超時時間
-
-
破壞循環等待條件:
-
對資源進行排序,按固定順序獲取鎖
-
使用資源分配圖算法檢測
-
7. volatile
?vs?synchronized
特性 | volatile | synchronized |
---|---|---|
可見性 | ? 保證可見性 | ? 保證可見性 |
原子性 | ? 不保證原子性(如?i++ ) | ? 保證原子性 |
指令重排序 | ? 禁止重排序 | ? 禁止重排序 |
性能 | 開銷較小,適合輕量級同步 | 開銷較大,適合重量級同步 |
使用場景 | 狀態標志、單次初始化、禁止重排序 | 復合操作、方法或代碼塊同步 |