在多線程編程中,“線程安全”是一個非常重要但又常被誤解的概念。尤其對于剛接觸多線程的人來說,不理解線程安全的本質,容易寫出“偶爾出錯”的代碼——這類 bug 往往隱蔽且難以復現。
本文將用盡可能通俗的語言,從三個角度解釋線程不安全的常見原因,并提供具體的示例和解決方法。
一、什么是線程安全?
線程安全(Thread Safety)簡單來說就是:
在多個線程同時執行某段代碼時,無論線程怎么調度、怎么交叉執行,都會得到正確的結果,不會出 bug。
線程安全:
多個線程訪問相同對象時,不會引起數據不一致或狀態混亂。
線程不安全:
同一段代碼,在單線程環境下一切正常,但在多線程環境下,結果可能出錯或不一致。
二、線程不安全的三大根源
1. 原子性問題:操作不是一步完成的
一個典型例子是變量自增 count++
。雖然看起來是一條語句,但其實底層是三條指令:
load // 從內存讀取 count 到 CPU 寄存器
add // 在寄存器中執行 +1 操作
store // 把結果寫回內存
在兩個線程同時執行 count++
時,可能會出現以下競態(race condition):
🎯 理想順序(無競態):線程串行執行
時間軸 →
Thread A:load(0)
?→?add(1)
?→?store(1)
Thread B:??load(1)
?→?add(1)
?→?store(2)
最終結果:
count = 2
(正確,無操作丟失)
? 競態情況 1:兩個線程都讀取了舊值(0)
時間軸 →
Thread A:load(0)
?→?add(1)
?----------------->?store(1)
Thread B:??load(0)
?→?add(1)
?----------------->?store(1)
最終結果:
count = 1
??(兩個線程都基于舊值 0 進行計算,結果被覆蓋,丟失了一次加法)
? 競態情況 2:交錯執行導致結果被覆蓋
時間軸 →
Thread A:load(0)
?→?add(1)
?----------------->?store(1)
Thread B:??load(0)
?→?add(1)
?→?store(1)
最終結果:
count = 1
??(Thread A 的存儲操作覆蓋了 Thread B 的結果)
? 競態情況 3:Thread B 插隊執行完畢
時間軸 →
Thread A:load(0)
?→?add(1)
Thread B:??load(0)
?→?add(1)
?→?store(1)
Thread A:??store(1)
最終結果:
count = 1
??(Thread A 最后寫入的值覆蓋了 Thread B 的加法結果)
? 競態情況 4:Thread A 讀值后等待,Thread B 先完成
時間軸 →
Thread A:load(0)
Thread B:??load(0)
?→?add(1)
?→?store(1)
Thread A:??add(1)
?→?store(1)
最終結果:
count = 1
??(兩個線程都基于同一個初始值 0 進行加法,導致一次加法丟失)
解決方案
-
使用
synchronized
同步關鍵代碼塊synchronized (this) {count++; }
-
使用原子類如
AtomicInteger
實現原子操作AtomicInteger count = new AtomicInteger(0); count.incrementAndGet(); // 原子性 +1
3. 指令重排序:執行順序被優化
為了提升性能,編譯器和 CPU 可能對指令重新排序,只要單線程語義不變即可。但這可能影響多線程環境的執行邏輯。
例如:
// 線程 A a = 1; flag = true;// 線程 B if (flag) {System.out.println(a); // 可能輸出 0! }
?
2. 可見性問題:變量更新對其他線程不可見
Java 中每個線程都有自己的工作內存(工作緩存),它會緩存主內存中的變量副本。這就導致:
-
一個線程修改了變量,另一個線程卻看不到。
例如:
volatile boolean running = true;public void stop() {running = false;
}public void run() {while (running) {// 執行某些操作}
}
由于重排序,可能發生 flag = true
提前執行,而 a = 1
還沒發生,導致 a
為默認值 0
。
🛠 解決方案:
-
使用
volatile
修飾flag
,防止指令重排; -
或使用
synchronized
,保證順序一致; -
對不可變對象,使用
final
修飾字段也是一種有效方式。
三、小結
線程安全問題,來源于我們“看似簡單”的代碼在多線程環境下可能出現的非預期行為:
-
原子性:多步操作被打斷
-
可見性:線程看不到最新值
-
指令重排:操作順序被調整
?
?