1.現象
當我們操作一個線程池的時候,可能需要去計數,也就是統計count,那我們這里有一個疑問,會不會產生線程安全問題?
毫無疑問絕對會有線程安全問題。在線程池環境中,多個線程并發訪問和修改一個共享的 count
變量(例如通過 count++
或 count = count + 1
),如果不加鎖或使用其他同步機制,會導致結果不可預測和不正確。
2.就像我們現在的這樣
根本原因分析:
- 非原子性:
count++
操作被拆分為 3 個獨立步驟 - 時間窗口:在讀取和寫入之間存在競爭窗口期
- 缺乏可見性:線程 B 看不到線程 A 的中間結果
- 寫覆蓋:后寫入的線程覆蓋了前一線程的結果
原因如下:
-
非原子性操作 (
count++
):- 像
count++
這樣看似簡單的操作,在底層通常需要多個步驟:- 讀取:從內存中讀取
count
的當前值到線程的寄存器或本地緩存。 - 修改:在寄存器/緩存中將讀取到的值加 1。
- 寫入:將修改后的新值寫回內存中的
count
變量。
- 讀取:從內存中讀取
- 這些步驟本身不是原子操作(不可分割的操作)。多個線程完全有可能交錯執行這些步驟。
- 像
-
競爭條件 (Race Condition):
- 假設
count
初始值為 0。 - 線程 A 執行步驟 1,讀取到
count = 0
。 - 線程 B 執行步驟 1,也讀取到
count = 0
(因為線程 A 還沒來得及寫回)。 - 線程 A 執行步驟 2,計算
0 + 1 = 1
。 - 線程 B 執行步驟 2,計算
0 + 1 = 1
。 - 線程 A 執行步驟 3,將
1
寫入count
,內存中count
變為 1。 - 線程 B 執行步驟 3,將
1
寫入count
,內存中count
還是 1(覆蓋了線程 A 的結果)。 - 結果:兩個線程都執行了
count++
,但最終count
的值是 1 而不是預期的 2。這就是經典的“丟失更新”問題。
- 假設
-
可見性問題 (Visibility):
- 現代 CPU 架構擁有多級緩存(L1, L2, L3)。每個線程可能在自己的 CPU 核心的緩存中操作
count
的副本。 - 當一個線程修改了它緩存中的
count
值,這個修改不會立即對其他線程的緩存可見。 - 線程 B 可能仍然看到
count
的舊值(比如 0),即使線程 A 已經把它加到了 1(但新值還在線程 A 的緩存里,沒刷回主內存或線程 B 的緩存沒更新)。 - 這也會導致線程 B 基于過時的值進行計算,最終結果錯誤。
- 現代 CPU 架構擁有多級緩存(L1, L2, L3)。每個線程可能在自己的 CPU 核心的緩存中操作
后果:
- 最終
count
的值會小于實際所有線程執行count++
操作的次數總和。丟失更新的次數越多,差距越大。 - 程序行為不可預測,結果每次運行都可能不同(取決于線程調度的時機)。
3.如何解決?
必須使用同步機制來保證對 count
的訪問和修改是原子性的,并且修改對其他線程是可見的:
-
使用
synchronized
關鍵字 (鎖):private int count = 0; private final Object lock = new Object(); // 專門用作鎖的對象public void increment() {synchronized (lock) { // 獲取鎖count++; // 在鎖保護的臨界區內安全地遞增} // 釋放鎖 }
- 優點:簡單直觀,適用于復雜的同步邏輯。
- 缺點:性能開銷相對較大(獲取/釋放鎖、線程阻塞/喚醒)。
-
使用
ReentrantLock
:private int count = 0; private final ReentrantLock lock = new ReentrantLock();public void increment() {lock.lock(); // 顯式獲取鎖try {count++;} finally {lock.unlock(); // 確保在finally塊中釋放鎖} }
- 優點:比
synchronized
更靈活(如可嘗試獲取鎖、可中斷鎖、公平鎖等)。 - 缺點:需要手動管理鎖的獲取和釋放,否則容易死鎖;性能開銷與
synchronized
接近或略優/劣(取決于場景和 JDK 版本)。
- 優點:比
-
使用原子類 (
java.util.concurrent.atomic
) - 強烈推薦用于計數器:private final AtomicInteger count = new AtomicInteger(0);public void increment() {count.incrementAndGet(); // 原子地遞增并返回新值// 或者 count.getAndIncrement(); // 原子地遞增并返回舊值 }
-
優點:性能最高!底層使用 CPU 提供的 CAS (Compare-And-Swap) 指令實現無鎖并發。特別適合簡單的計數器場景。
-
缺點:只能用于特定的原子操作(遞增、遞減、加法、比較并設置等)。對于需要保護多個變量或復雜邏輯的復合操作,原子類可能不夠用,需要用鎖。
解決方案對比:
方法 原理 性能影響 適用場景 synchronized 互斥鎖 高 (上下文切換) 復雜同步邏輯 AtomicInteger CAS 指令 低 (CPU 原語) 簡單計數器 ReentrantLock 可重入鎖 中 (優于 synchronized) 需要靈活控制的場景 -
4.結論:
在線程池(或任何多線程環境)中,對共享可變狀態(如你的 count
)進行并發修改,必須使用適當的同步機制(鎖或原子類)。不采取任何同步措施必然會導致線程安全問題,使 count
的值不可靠。
對于簡單的計數器場景,優先考慮 AtomicInteger
或 AtomicLong
,它們提供了最佳的性能和簡潔性。