線程安全
原子性(Atomicity)、可見性(Visibility)、有序性(Ordering)?是保證線程安全的三大核心要素 —— 線程安全問題的本質,幾乎都是這三個特性中的一個或多個被破壞導致的。
- 操作不會被 “中途打斷”(原子性);
- 操作結果能被其他線程 “及時看見”(可見性);
- 操作順序符合 “語義邏輯”(有序性)。
可見性:
可見性問題指:一個線程對共享變量的修改,其他線程可能無法立即看到,甚至永遠看不到。
為什么影響線程安全?
若可見性被破壞,線程會基于 “舊值” 做決策或修改,導致邏輯錯誤。
現代 CPU 為提升效率,引入了多級緩存(L1、L2、L3),線程的 “工作內存” 本質是 CPU 緩存的抽象。當線程修改變量時:
- 步驟 1:修改 CPU 緩存中的副本(工作內存)。
- 步驟 2:CPU 會在 “合適的時機”(而非立即)將緩存中的新值刷新到主內存(如緩存滿了、發生緩存一致性協議觸發時)。
這就導致:若線程 A 剛修改了變量但未刷新到主內存,線程 B 從主內存讀取的仍是舊值,出現可見性問題。
可見性問題的產生:
當線程 A 修改了共享變量?A
?時,會先更新自己的工作內存,再異步刷新到主內存(這個過程有延遲)。若此時線程 B 讀取變量?A
,可能直接從自己的工作內存中獲取未被更新的老值(因為線程 A 的修改還未同步到主內存,或線程 B 未從主內存重新加載),導致兩個線程看到的變量值不一致。
// 共享變量
private static boolean flag = false;// 線程1:修改flag
new Thread(() -> {flag = true; // 修改工作內存中的flag,尚未同步到主內存System.out.println("線程1已修改flag為true");
}).start();// 線程2:讀取flag
new Thread(() -> {while (!flag) { // 可能一直讀取自己工作內存中的老值(false),陷入死循環// 等待flag變為true}System.out.println("線程2檢測到flag為true");
}).start();
線程 2 可能永遠看不到線程 1 對?flag
?的修改,因為?flag
?的更新未及時同步到主內存,或線程 2 未重新從主內存加載。
volatile解決:
volatile
?保證可見性的核心邏輯是:強制線程對變量的讀寫操作直接與主內存交互,跳過工作內存(CPU 緩存)的緩存優化,具體通過以下兩步實現:
寫操作時:立即刷新到主內存,并使其他線程的緩存失效
當線程修改一個?volatile
?變量時,JVM 會觸發兩個動作:- 強制將工作內存中該變量的新值立即刷新到主內存(不等待 CPU 緩存的 “合適時機”)。
- 通過 CPU 的緩存一致性協議(如 MESI 協議),通知其他線程中該變量的緩存副本失效(其他線程再讀取時必須從主內存重新加載)。
類比:
volatile
?變量的寫操作相當于 “寫完立即把筆記本內容抄回公共白板,并擦掉其他人筆記本上的舊內容”。讀操作時:必須從主內存重新加載
當線程讀取?volatile
?變量時,JVM 會強制線程放棄工作內存中的緩存副本,直接從主內存加載最新值。類比:
volatile
?變量的讀操作相當于 “每次看內容前,都先扔掉自己的筆記本,重新從公共白板抄最新內容”。
private static volatile boolean flag = false; // 用volatile修飾// 線程1修改后,會立即刷新到主內存,并使線程2的緩存失效
// 線程2讀取時,會從主內存重新加載,感知到flag的最新值
synchronized解決
核心機制是加鎖和解鎖時的內存同步操作
加鎖時(進入同步塊):
線程會清空自己的工作內存,并從主內存重新加載共享變量的最新值到工作內存。
(類比:線程進入同步塊前,先把自己的 “筆記本” 清空,重新從 “公共白板” 抄最新內容。)解鎖時(退出同步塊):
線程會將工作內存中修改后的共享變量值強制刷新到主內存。
(類比:線程退出同步塊時,必須把 “筆記本” 的修改立即抄回 “公共白板”。)happens-before 原則:
對同一個鎖,解鎖操作 happens-before 后續的加鎖操作。即:前一個線程解鎖時刷新到主內存的變量值,后一個線程加鎖時必然能從主內存讀到這個最新值。
// 共享變量(無volatile)
private static boolean flag = false;public static void main(String[] args) {// 線程1:修改flagnew Thread(() -> {synchronized (Test.class) { // 加鎖:從主內存加載flag(初始false)flag = true; // 修改工作內存中的flag} // 解鎖:將flag=true刷新到主內存}).start();// 線程2:讀取flagnew Thread(() -> {while (true) {synchronized (Test.class) { // 加鎖:從主內存加載flag的最新值if (flag) {System.out.println("線程2讀取到flag=true");break;}} // 解鎖:無修改,不影響}}).start();
}
- 線程 1 解鎖時,
flag=true
?被強制刷新到主內存。 - 線程 2 每次加鎖時,都會從主內存重新加載?
flag
,因此必然能感知到?flag
?的修改,最終退出循環。
有序性
有序性指程序執行順序符合代碼的 “語義邏輯順序”,避免編譯器 / CPU 的指令重排序破壞線程間的依賴關系。
為什么影響線程安全?
重排序可能打破線程間的 “操作先后依賴”,導致基于順序的邏輯判斷失效。
// 共享變量
private static int a = 0;
private static boolean flag = false;// 線程1:先初始化a,再標記flag
new Thread(() -> {a = 1; // 操作1:初始化aflag = true; // 操作2:標記a已初始化
}).start();// 線程2:基于flag判斷a是否可用
new Thread(() -> {if (flag) { // 若flag=true,認為a已初始化System.out.println(a); // 可能輸出0(因重排序)}
}).start();
若線程 1 的操作 1 和操作 2 被重排序(先執行?flag=true
,再執行?a=1
),線程 2 會在?a
?未初始化時讀取,輸出 0(不符合預期),破壞線程安全。
volatile解決有序性:
volatile
?通過在變量的讀寫操作前后插入內存屏障(Memory Barrier)?來禁止特定類型的重排序,從而保證有序性。內存屏障是一種特殊的指令,它會阻止編譯器和 CPU 對屏障兩側的指令進行重排序。
volatile 內存屏障的具體規則
操作類型 | 內存屏障插入位置 | 作用 |
---|---|---|
寫操作(v = x) | 寫操作前插入?StoreStore 屏障 | 禁止當前寫操作與之前的其他寫操作重排序(確保之前的寫操作先于當前寫操作執行)。 |
寫操作后插入?StoreLoad 屏障 | 禁止當前寫操作與之后的讀 / 寫操作重排序(確保當前寫操作完成后,再執行后續操作)。 | |
讀操作(x = v) | 讀操作前插入?LoadLoad 屏障 | 禁止當前讀操作與之前的其他讀操作重排序(確保之前的讀操作先于當前讀操作執行)。 |
讀操作后插入?LoadStore 屏障 | 禁止當前讀操作與之后的寫操作重排序(確保當前讀操作完成后,再執行后續寫操作)。 |
這些屏障的核心作用是 “隔離屏障兩側的指令”,確保?volatile
?變量的讀寫操作不會與其他指令 “交叉執行”。
synchronized解決有序性:?
synchronized
?通過?“happens-before 原則”?和?“同步塊的邊界約束”?保證有序性。其核心邏輯是:同步塊內的操作會被視為一個 “不可分割的整體”,不會與同步塊外的操作重排序,且后續線程進入同步塊時,能看到之前同步塊內的所有操作結果。
同步塊內的操作不會被重排序到塊外
JMM 規定:編譯器和 CPU 不得將同步塊內的指令重排序到同步塊外部(無論是進入塊前還是退出塊后)。例如:synchronized (lock) { // 加鎖a = 1; // 同步塊內操作1flag = true; // 同步塊內操作2 } // 解鎖
編譯器和 CPU 不能將?
a=1
?或?flag=true
?重排序到?synchronized
?塊外部,確保同步塊內的操作順序嚴格按代碼執行。happens-before 關系保證跨線程可見性與順序性
JMM 的 happens-before 原則規定:對同一個鎖的解鎖操作 happens-before 后續的加鎖操作。即:- 線程 A 退出同步塊(解鎖)時,其在同步塊內的所有操作(如?
a=1
、flag=true
)都會被刷新到主內存。 - 線程 B 進入同步塊(加鎖)時,會從主內存加載所有變量的最新值,因此能看到線程 A 在同步塊內的所有操作結果。
這種關系確保了:線程 A 的操作 “先行發生于” 線程 B 的操作,兩者的執行順序在邏輯上是有序的。
- 線程 A 退出同步塊(解鎖)時,其在同步塊內的所有操作(如?
原子性:
原子性指一個操作(或多個操作的組合)要么全部執行,要么全部不執行,不會被其他線程 “打斷”,中間狀態不會被暴露
典型案例:非原子操作的風險
以?count++
?為例,這是一個看似簡單的操作,但在底層會被拆分為 3 個步驟:
- 讀取:從主內存讀取?
count
?的當前值到線程的工作內存。 - 修改:在工作內存中對?
count
?加 1。 - 寫入:將修改后的值刷新回主內存。
當兩個線程同時執行?count++
?時,可能出現以下交叉執行的情況:
- 線程 A 讀取?
count=0
?→ 線程 B 讀取?count=0
(此時兩者都在步驟 1)。 - 線程 A 加 1 后?
count=1
?→ 線程 B 加 1 后?count=1
(步驟 2)。 - 線程 A 寫入主內存?
count=1
?→ 線程 B 寫入主內存?count=1
(步驟 3)。
基于鎖機制解決:
private int count = 0;// 用synchronized修飾方法,保證count++的原子性
public synchronized void increment() {count++; // 復合操作被synchronized保護,不會被其他線程中斷
}
線程進入?synchronized
?方法 / 塊時必須獲取鎖,執行完成后釋放鎖。同一時間只有一個線程能持有鎖,確保臨界區內的操作不會被其他線程打斷。
使用原子類:
CAS 機制(Atomic
?系列類):通過硬件指令實現無鎖原子操作,適合簡單場景,性能更優。