在多線程編程中,線程安全問題就像隱藏在代碼中的定時炸彈,隨時可能引發難以調試的 bug。本文將帶你深入理解線程安全問題的本質,並通過實例分析幾種常用的解決方案,幫助你構建健壯的多線程應用。
一、什麼是線程安全問題?
當多個線程同時訪問共享資源(變量、對象等)並且至少有一個線程會修改該資源時,如果沒有正確的同步機制,就可能產生數據不一致的問題。這就是我們常説的"線程不安全"。
二、Java 內存模型(JMM)基礎
在理解線程安全問題之前,我們需要了解 Java 內存模型(Java Memory Model, JMM)的基本概念。JMM 定義了線程和主內存之間的抽象關係,規定了如何處理可見性、原子性和有序性問題。
想象一下一個教室:主內存就像教室裏的大黑板,所有人都可以看到;而每個線程有自己的小黑板(工作內存),只有自己能看到。線程要修改共享變量,必須先從大黑板抄到自己的小黑板,修改後再寫回大黑板,而其他線程要看到這個修改,必須重新從大黑板抄寫到自己的小黑板上。
更技術性地説:
- 所有變量都存儲在主內存中
- 每個線程有自己的工作內存(類似於 CPU 緩存),保存了被該線程使用的變量的主內存副本
- 線程對變量的所有操作都必須在工作內存中進行,而不能直接操作主內存
- 不同線程之間無法直接訪問對方工作內存中的變量
這種設計導致了線程安全的三個核心問題:
- 可見性:一個線程修改了變量值,其他線程能否立即看到
- 原子性:一個操作是否可以被中斷
- 有序性:代碼執行順序是否會被重排序優化
三、線程安全問題的根源:競態條件
競態條件(Race Condition)是指多個線程以不可預期的順序訪問共享資源,導致程序結果依賴於線程執行的時序。
經典案例:計數器問題
看下面這個看似簡單的計數器代碼:
public class UnsafeCounter {
private int count = 0;
public void increment() {
count++; // 看似是原子操作,實際不是
}
public int getCount() {
return count;
}
public static void main(String[] args) throws InterruptedException {
UnsafeCounter counter = new UnsafeCounter();
Thread[] threads = new Thread[100];
// 創建100個線程,每個線程將計數器加1000次
for (int i = 0; i < threads.length; i++) {
threads[i] = new Thread(() -> {
for (int j = 0; j < 1000; j++) {
counter.increment();
}
});
threads[i].start();
}
// 等待所有線程執行完畢
for (Thread thread : threads) {
thread.join();
}
// 理論上結果應該是100,000
System.out.println("Count: " + counter.getCount());
}
}
運行這段代碼,你會發現最終結果很可能小於 100,000。為什麼?
深入分析:count++不是原子操作
count++看起來很簡單,但實際上它包含三個步驟:
- 讀取 count 的當前值
- 將值加 1
- 將結果寫回 count
如上圖所示,當兩個線程同時執行count++時,可能會出現一個線程的操作被另一個線程覆蓋的情況,這就導致了計數器的值小於預期。
四、解決方案一:synchronized 關鍵字
Java 提供了synchronized關鍵字來解決線程安全問題,它能夠確保同一時刻只有一個線程可以執行被保護的代碼塊。
synchronized 的三種使用方式
- 同步實例方法:鎖定當前對象實例
public synchronized void increment() {
count++;
}
- 同步靜態方法:鎖定類對象
public static synchronized void staticMethod() {
// 靜態變量操作
}
- 同步代碼塊:可以指定鎖對象,更加靈活
public void increment() {
synchronized(this) {
count++;
}
}
使用 synchronized 解決計數器問題
public class SafeCounter {
private int count = 0;
public synchronized void increment() {
count++;
}
public synchronized int getCount() {
return count;
}
public static void main(String[] args) throws InterruptedException {
SafeCounter counter = new SafeCounter();
Thread[] threads = new Thread[100];
for (int i = 0; i < threads.length; i++) {
threads[i] = new Thread(() -> {
for (int j = 0; j < 1000; j++) {
counter.increment();
}
});
threads[i].start();
}
for (Thread thread : threads) {
thread.join();
}
System.out.println("Count: " + counter.getCount()); // 結果始終為100,000
}
}
説明:getCount()方法也添加了synchronized關鍵字,這是因為在多線程環境下,讀操作也需要同步以確保獲取到最新的結果。如果count變量不會被修改(只讀),則可以不加鎖;但在頻繁修改的場景下,讀取時必須同步以保證可見性。
synchronized 的工作原理與鎖升級
在 JVM 中,每個對象都有一個關聯的 Monitor(監視器)。當線程進入 synchronized 塊時,它會嘗試獲取 Monitor 的所有權:
在 JDK 6 之後,HotSpot JVM 引入了鎖升級機制(也稱為偏向鎖、輕量級鎖和重量級鎖):
- 偏向鎖:適用於只有一個線程訪問同步塊的情況。首次獲得鎖時,記錄線程 ID,後續該線程再次進入時無需獲取鎖,直接執行。
- 輕量級鎖:當有第二個線程嘗試獲取偏向鎖時,鎖會升級為輕量級鎖。通過 CAS 操作(比較並交換,一種硬件層面的原子操作)嘗試獲取鎖,如果失敗則自旋一定次數,避免線程阻塞。
- 重量級鎖:如果自旋超過閾值或有多個線程同時競爭鎖,則升級為重量級鎖。此時,未獲得鎖的線程將被阻塞,避免 CPU 空轉。
這種自適應的鎖機制大大提高了 synchronized 在不同競爭場景下的性能,使得 JDK 6 之後的 synchronized 性能顯著提升。
五、解決方案二:volatile 關鍵字
volatile關鍵字是解決可見性和有序性問題的利器,但它不能解決原子性問題。
volatile 的作用
- 可見性保證:當一個線程修改了 volatile 變量的值,這個新值對其他線程是立即可見的
- 有序性保證:防止指令重排序優化
指令重排序是編譯器和處理器為了提高性能而進行的優化,它們可能會改變語句的執行順序,但保證單線程情況下結果一致。然而在多線程環境下,這種重排序可能導致意外的行為。volatile 關鍵字通過內存屏障(Memory Barrier)阻止特定範圍內的指令重排序。
volatile 適用場景
volatile 主要適用於獨立變量的可見性保證,特別是在以下場景:
- 狀態標誌:線程間共享的狀態標誌
public class TaskRunner {
private volatile boolean running = false;
public void start() {
running = true;
new Thread(() -> {
while (running) {
// 執行任務
}
}).start();
}
public void stop() {
running = false; // 通知工作線程停止
}
}
- 雙重檢查鎖定模式:實現單例模式
public class Singleton {
private static volatile Singleton instance;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
為什麼這裏需要 volatile? 對象創建過程包含三個步驟:① 分配內存空間 ② 初始化對象 ③ 引用指向內存空間。由於指令重排序,可能導致步驟 ③ 在 ② 之前執行,使其他線程看到未完全初始化的對象。volatile 可以防止這種重排序,確保對象完全初始化後才能被其他線程訪問。
volatile 的侷限性
volatile 不能保證原子性,看下面這個例子:
public class VolatileCounter {
private volatile int count = 0;
public void increment() {
count++; // 即使count是volatile,這也不是原子操作
}
public int getCount() {
return count;
}
}
這個代碼依然存在線程安全問題,因為count++不是原子操作,volatile 只能保證count的值對所有線程可見,但不能保證讀-改-寫過程的原子性。
volatile 的內存語義
當寫入 volatile 變量時,JMM 會插入一個寫屏障(Store Barrier),當讀取 volatile 變量時,JMM 會插入一個讀屏障(Load Barrier)。內存屏障是一種 CPU 指令,用於控制特定條件下的內存操作順序,確保多線程環境下的內存可見性和有序性。這些屏障的存在確保了 volatile 變量的可見性和有序性。
六、解決方案三:原子類
Java 提供了java.util.concurrent.atomic包,裏面包含了一系列支持原子操作的類,如AtomicInteger、AtomicLong等。這些類適用於需要原子性的複合操作,如計數器、累加器等。Java 8 還引入了LongAdder和LongAccumulator等性能更高的原子類,適用於高併發場景。
原子類原理:volatile + CAS 的結合
原子類內部實現了兩層保障:
- 使用
volatile修飾的變量保證可見性 - 使用 CAS(Compare And Swap)操作保證原子性
CAS 是一種樂觀鎖技術,可以理解為"看-比較-再操作"的過程。就像你去取一本圖書館的書,離開座位前記下書的位置,回來後先檢查書是否還在原處,如果是才能拿走,否則需要重新查找書的新位置。
以AtomicInteger為例,其內部實現大致如下:
public class AtomicInteger extends Number implements java.io.Serializable {
private volatile int value; // 注意這裏使用volatile
// 原子性地設置新值
public final int incrementAndGet() {
for (;;) {
int current = get();
int next = current + 1;
if (compareAndSet(current, next))
return next;
}
}
// CAS操作,由CPU原子指令支持
public final boolean compareAndSet(int expect, int update) {
// 底層調用Unsafe的native方法
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
}
這種volatile + CAS的組合保證了原子類同時具備可見性和原子性。
原子類的適用邊界
需要注意,原子類主要適用於單個變量的原子操作,但對於涉及多個變量的複合操作,原子類仍然無法保證整體的原子性。例如:
// 這種複合操作不能僅靠原子類保證原子性
public void transferMoney(Account from, Account to, int amount) {
// 即使賬户餘額使用AtomicInteger,這裏仍需要額外同步
from.getBalance().addAndGet(-amount);
to.getBalance().addAndGet(amount);
}
上面的代碼即使使用了原子類,整個轉賬操作仍然不是原子的,因為它涉及兩個獨立變量的更新。在這種情況下,仍然需要使用synchronized或Lock:
public void transferMoney(Account from, Account to, int amount) {
synchronized(this) {
from.getBalance().addAndGet(-amount);
to.getBalance().addAndGet(amount);
}
}
CAS 操作原理
CAS 是一種無鎖算法,其基本思想是:
- 讀取當前值(假設為 A)
- 基於當前值計算新值(B)
- 如果當前值仍為 A,則更新為 B,否則操作失敗
- 如果失敗,則重試或返回
使用 AtomicInteger 解決計數器問題
import java.util.concurrent.atomic.AtomicInteger;
public class AtomicCounter {
private AtomicInteger count = new AtomicInteger(0);
public void increment() {
count.incrementAndGet(); // 原子操作
}
public int getCount() {
return count.get();
}
public static void main(String[] args) throws InterruptedException {
AtomicCounter counter = new AtomicCounter();
Thread[] threads = new Thread[100];
for (int i = 0; i < threads.length; i++) {
threads[i] = new Thread(() -> {
for (int j = 0; j < 1000; j++) {
counter.increment();
}
});
threads[i].start();
}
for (Thread thread : threads) {
thread.join();
}
System.out.println("Count: " + counter.getCount()); // 結果始終為100,000
}
}
CAS 的侷限性
雖然 CAS 操作高效,但也存在一些侷限性:
- ABA 問題:如果一個值從 A 變為 B,又從 B 變回 A,使用 CAS 操作的線程可能誤認為該值未被修改過。解決方法是使用
AtomicStampedReference,它不僅比較值,還比較版本號。
// 解決ABA問題的示例
AtomicStampedReference<Integer> atomicRef = new AtomicStampedReference<>(100, 0);
// 獲取當前值和版本號
int[] stampHolder = new int[1];
Integer initialValue = atomicRef.get(stampHolder);
int initialStamp = stampHolder[0];
// 基於版本號更新
atomicRef.compareAndSet(initialValue, 200, initialStamp, initialStamp + 1);
- 高競爭下的性能問題:在高併發環境下,如果多個線程反覆嘗試 CAS 操作卻失敗,會導致 CPU 資源浪費(稱為"自旋")。此時,
synchronized的阻塞機制反而可能更有效率。
原子類的高級操作
原子類不僅提供了基本的原子操作,還提供了一些高級功能:
- 累積操作:
addAndGet()、getAndAdd() - 條件更新:
compareAndSet() - 複合操作:
updateAndGet()、accumulateAndGet()
// 複合操作示例
atomicInt.updateAndGet(x -> x < 100 ? x + 1 : 100);
七、解決方案四:Lock 接口
除了synchronized,Java 還提供了更加靈活的java.util.concurrent.locks.Lock接口及其實現類,如ReentrantLock。這些顯式鎖是 JDK 5 引入的,為開發者提供了比內置鎖更多的控制選項。
ReentrantLock 的特點
- 可中斷鎖獲取:
lockInterruptibly()方法允許在等待鎖時響應中斷 - 超時鎖獲取:
tryLock(long timeout, TimeUnit unit)支持等待超時 - 公平鎖選項:可以創建公平鎖,按照線程等待的時間順序獲取鎖
- 條件變量:支持多個等待隊列,實現更精細的線程通信
Lock 接口的底層實現:AQS 框架
Lock 接口的實現類(如 ReentrantLock)內部依賴於 AQS(AbstractQueuedSynchronizer)框架。AQS 是 Java 併發包的基礎框架,它通過一個雙向隊列管理等待的線程,實現了鎖的獲取與釋放、線程排隊、阻塞與喚醒等核心功能。可以將 AQS 想象為一個管理"排隊線程"的系統,就像銀行櫃枱前的排號系統,決定哪個線程可以獲取資源,哪些需要等待。
AQS 支持獨佔模式(如 ReentrantLock)和共享模式(如 ReadWriteLock、CountDownLatch),這種統一的底層實現使得各種同步器在行為上保持一致性。
使用 ReentrantLock 的示例
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class LockCounter {
private final Lock lock = new ReentrantLock();
private int count = 0;
public void increment() {
lock.lock(); // 獲取鎖
try {
count++;
} finally {
lock.unlock(); // 確保鎖被釋放
}
}
public int getCount() {
lock.lock();
try {
return count;
} finally {
lock.unlock();
}
}
}
公平鎖與性能權衡
ReentrantLock允許創建公平鎖:
private final ReentrantLock fairLock = new ReentrantLock(true); // 公平鎖
公平鎖通過隊列機制確保線程按照請求鎖的順序獲取鎖,防止線程飢餓問題。然而,這種公平性是有代價的:
- 吞吐量降低:公平鎖會導致更多的上下文切換,降低整體吞吐量(通常性能比非公平鎖低 10%-30%)
- 響應時間增加:線程必須等待隊列前面的所有線程
- 適用場景:當線程等待時間的公平性比系統吞吐量更重要時使用
在大多數情況下,默認的非公平鎖(new ReentrantLock())性能更好,除非應用對鎖獲取順序有嚴格要求。
讀寫鎖:ReentrantReadWriteLock
在讀多寫少的場景下,可以使用ReentrantReadWriteLock進一步提高性能。Java 8 還引入了性能更高的StampedLock,它提供了樂觀讀模式,進一步優化了讀多寫少的場景。
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class ReadWriteCounter {
private final ReadWriteLock rwLock = new ReentrantReadWriteLock();
private int count = 0;
public void increment() {
rwLock.writeLock().lock(); // 寫鎖,獨佔式
try {
count++;
} finally {
rwLock.writeLock().unlock();
}
}
public int getCount() {
rwLock.readLock().lock(); // 讀鎖,共享式,多個線程可同時持有
try {
return count;
} finally {
rwLock.readLock().unlock();
}
}
}
讀寫鎖允許多個讀線程同時訪問,但寫線程必須獨佔,這在讀操作遠多於寫操作的場景下非常高效。
注意:雖然讀寫鎖在讀多寫少場景下性能優秀,但也存在潛在風險:寫鎖會阻塞所有讀鎖,如果一個線程長時間持有寫鎖,可能導致讀線程飢餓。使用時需控制寫操作的粒度,避免長時間持有寫鎖。也可以考慮使用公平模式的讀寫鎖緩解此問題。
八、volatile 與原子類:如何選擇?
在處理線程安全問題時,volatile和原子類的選擇取決於具體操作:
| 操作類型 | 推薦機制 | 示例 |
|---|---|---|
| 單一變量讀/寫(無複合操作) | volatile |
狀態標誌、配置項 |
| 讀-改-寫複合操作 | 原子類 | 計數器、累加器 |
| 多變量的關聯操作 | synchronized/Lock |
轉賬、交換值 |
簡單記憶:
- 只需可見性(讀/寫),用
volatile - 需要原子性(讀-改-寫),用原子類
- 需要多變量協同,用
synchronized/Lock
九、synchronized vs Lock:如何選擇?
synchronized和Lock兩種機制各有優缺點,如何選擇取決於具體需求:
synchronized 的優勢
- 語法簡潔:作為關鍵字,使用更簡單
- 自動鎖管理:不需要手動釋放鎖,避免忘記 unlock 導致的死鎖
- JVM 優化:現代 JVM 對 synchronized 進行了大量優化,性能已經非常好
Lock 的優勢
- 更多控制選項:支持中斷、超時、公平性
- 多條件等待:支持多個條件變量
- 非阻塞嘗試:tryLock()可以嘗試獲取鎖但不阻塞
實際選擇依據
- 簡單場景,沒有特殊需求:優先使用
synchronized(代碼簡潔,JVM 優化好) -
需要下列特性時,選擇
Lock:- 需要可中斷的鎖獲取
- 需要超時的鎖獲取
- 需要公平鎖
- 需要多個條件變量
- 需要非阻塞的嘗試獲取鎖
// 複雜場景使用Lock的示例
public class ComplexResourceManager {
private final ReentrantLock lock = new ReentrantLock(true); // 公平鎖
private final Condition notEmpty = lock.newCondition();
private final Condition notFull = lock.newCondition();
private final Queue<Task> tasks = new LinkedList<>();
private final int capacity = 10;
public boolean addTask(Task task, long timeout, TimeUnit unit)
throws InterruptedException {
lock.lockInterruptibly(); // 可中斷鎖
try {
long nanos = unit.toNanos(timeout);
while (tasks.size() == capacity) {
if (nanos <= 0)
return false; // 超時返回
nanos = notFull.awaitNanos(nanos); // 等待指定時間
}
tasks.add(task);
notEmpty.signal(); // 通知等待的消費者
return true;
} finally {
lock.unlock();
}
}
// 其他方法...
}
十、不可變對象:設計層面的線程安全
除了同步機制,另一種實現線程安全的方式是使用不可變對象。不可變對象在創建後其狀態不能被修改,這從根本上避免了線程安全問題。
不可變對象的線程安全性
不可變對象的線程安全性來自於其"狀態不可變"的特性:
- 創建後狀態不能被修改
- 所有字段都是 final(確保初始化後不會改變)
- 對象不提供修改狀態的方法
- 如果包含可變對象引用,不允許修改這些對象
Java 標準庫中的許多類都是不可變的,如String、Integer、BigDecimal等。
深度不可變:處理可變引用
即使字段聲明為final,如果該字段引用的是可變對象(如集合),仍然需要特別注意:
// 深度不可變的正確實現
public final class ImmutableCollection {
private final List<String> values; // final只保證引用不變,但List內容可變
public ImmutableCollection(List<String> initialValues) {
// 創建防禦性副本,避免構造函數中的參數被外部修改
this.values = new ArrayList<>(initialValues);
}
public List<String> getValues() {
// 返回不可變視圖或副本,防止外部修改
return Collections.unmodifiableList(values);
// 或者: return new ArrayList<>(values);
}
}
如不採取上述措施,外部代碼仍可修改對象內部狀態,破壞不可變性。
創建不可變類的示例
// 不可變類示例
public final class ImmutablePoint {
private final int x; // final字段
private final int y;
public ImmutablePoint(int x, int y) {
this.x = x;
this.y = y;
}
public int getX() { return x; }
public int getY() { return y; }
// 操作返回新對象,不修改當前對象
public ImmutablePoint translate(int dx, int dy) {
return new ImmutablePoint(x + dx, y + dy);
}
}
這種設計模式自然線程安全,無需任何同步機制,特別適合作為共享狀態或緩存數據。
十一、實際應用案例分析
案例 1:共享計數器
需求:多線程環境下統計網站訪問量
解決方案:使用AtomicLong實現
public class PageViewCounter {
private final AtomicLong viewCount = new AtomicLong(0);
public void increment() {
viewCount.incrementAndGet();
}
public long getCount() {
return viewCount.get();
}
}
案例 2:狀態標誌
需求:控制工作線程的運行狀態
解決方案:使用volatile變量
public class WorkerManager {
private volatile boolean running = true;
private final List<Thread> workers = new ArrayList<>();
public void startWorkers(int count) {
for (int i = 0; i < count; i++) {
Thread worker = new Thread(() -> {
while (running) {
processTask();
}
});
workers.add(worker);
worker.start();
}
}
public void stopAll() {
running = false;
}
private void processTask() {
// 處理任務
}
}
案例 3:緩存服務
需求:實現一個線程安全的緩存服務
解決方案:使用ReadWriteLock提高併發讀取性能
public class ConcurrentCache<K, V> {
private final Map<K, V> cache = new HashMap<>();
private final ReadWriteLock rwLock = new ReentrantReadWriteLock();
private final Lock readLock = rwLock.readLock();
private final Lock writeLock = rwLock.writeLock();
public V get(K key) {
readLock.lock();
try {
return cache.get(key);
} finally {
readLock.unlock();
}
}
public void put(K key, V value) {
writeLock.lock();
try {
cache.put(key, value);
} finally {
writeLock.unlock();
}
}
public boolean contains(K key) {
readLock.lock();
try {
return cache.containsKey(key);
} finally {
readLock.unlock();
}
}
public V remove(K key) {
writeLock.lock();
try {
return cache.remove(key);
} finally {
writeLock.unlock();
}
}
}
十二、線程安全問題的預防與檢測
預防措施
- 儘量使用不可變對象:不可變對象天生線程安全
// 使用不可變對象的示例
public void processUserData(String userId) {
// String是不可變的,多線程共享也安全
String cacheKey = "user:" + userId;
UserData userData = getUserData(cacheKey);
// ...
}
- 使用線程安全的集合:如
ConcurrentHashMap、CopyOnWriteArrayList
// 使用線程安全集合
private final Map<String, User> userCache = new ConcurrentHashMap<>();
private final List<String> accessLog = new CopyOnWriteArrayList<>();
這些線程安全集合的底層實現各不相同:
ConcurrentHashMap:在 Java 7 中使用分段鎖(Segment),Java 8 後改為 CAS+synchronized+紅黑樹實現高併發性能CopyOnWriteArrayList:寫操作時複製整個數組,適合讀多寫少場景ConcurrentLinkedQueue:使用 CAS 實現的無鎖隊列,適合高併發場景
- 遵循封裝原則:不要暴露可變的共享狀態
// 不要這樣做
public List<Task> getTasks() {
return tasks; // 直接返回內部集合,允許外部修改
}
// 正確做法
public List<Task> getTasks() {
return new ArrayList<>(tasks); // 返回副本
}
- 使用局部變量:減少共享狀態
public void processRequest(Request request) {
// 局部變量,線程封閉,無需同步
int count = 0;
StringBuilder builder = new StringBuilder();
// ...
}
- 使用 ThreadLocal:當數據需要線程隔離時
public class ThreadLocalExample {
// 每個線程都有自己的SimpleDateFormat實例
private static final ThreadLocal<SimpleDateFormat> dateFormatHolder =
ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd"));
public String formatDate(Date date) {
// 獲取當前線程的SimpleDateFormat實例
return dateFormatHolder.get().format(date);
}
}
調試與檢測工具
- Thread Dump:使用
jstack <pid>命令獲取線程轉儲信息
# 查找Java進程ID
jps
# 生成線程轉儲
jstack 12345 > thread-dump.txt
通過查看轉儲文件,可以識別死鎖、阻塞和鎖競爭情況。
-
Java VisualVM:可視化監控線程狀態
- 下載並啓動 JVisualVM
- 連接到目標應用程序
- 在"線程"標籤中查看線程狀態、CPU 使用率和鎖爭用
-
FindBugs/SpotBugs:靜態代碼分析工具
MT_CORRECTNESS檢查項可以檢測多線程代碼中的常見錯誤- 例如:未同步的共享字段訪問、雙重檢查鎖定錯誤等
十三、總結
下表總結了四種線程安全解決方案的特點和適用場景:
| 特性 | synchronized | volatile | 原子類 | Lock 接口 |
|---|---|---|---|---|
| 原子性 | ✓ | ✗ | ✓ | ✓ |
| 可見性 | ✓ | ✓ | ✓ | ✓ |
| 有序性 | ✓ | ✓ | ✓ | ✓ |
| 性能開銷 | 中等(自適應升級) | 低 | 中等(高競爭時高) | 中等 |
| 適用場景 | 複雜共享狀態 | 狀態標誌,可見性需求 | 計數器,累加器 | 需要更靈活控制的場景 |
| 死鎖風險 | 有 | 無 | 無 | 有 |
| 粒度控制 | 靈活 | 只能應用於變量 | 只能應用於變量 | 最靈活 |
| 是否阻塞線程 | 是(重量級鎖) | 否 | 否(自旋) | 是 |
| 超時/中斷 | 不支持 | 不適用 | 不適用 | 支持 |
| 公平性選擇 | 不支持 | 不適用 | 不適用 | 支持 |
| 多條件等待 | 不支持 | 不適用 | 不適用 | 支持 |
| 鎖升級機制 | 支持 | 不適用 | 不適用 | 不支持 |
| 實現難度 | 簡單 | 簡單 | 簡單 | 中等 |
| 內部實現 | 監視器 | 內存屏障 | volatile + CAS | AQS 框架 |
線程安全問題是 Java 多線程編程中最關鍵的挑戰之一,理解並掌握這些基本解決方案,對編寫健壯的併發程序至關重要。每種方案都有其適用場景,選擇合適的同步機制需要考慮多方面因素,包括性能需求、代碼複雜度和維護性等。
希望本文能幫助你深入理解 Java 線程安全問題,並在實際開發中做出明智的技術選擇!
在下一篇文章中,我們將探討 synchronized 深度解析與鎖優化,敬請期待!
感謝您耐心閲讀到這裏!如果覺得本文對您有幫助,歡迎點贊 👍、收藏 ⭐、分享給需要的朋友,您的支持是我持續輸出技術乾貨的最大動力!
如果想獲取更多 Java 技術深度解析,歡迎點擊頭像關注我,後續會每日更新高質量技術文章,陪您一起進階成長~