名詞解釋:
指令重排是計算機為了優化執行效率,在不改變單線程程序結果的前提下,對代碼的執行順序進行重新排列的操作。它可能發生在編譯階段(編譯器優化)或CPU運行階段(處理器優化)。
舉個栗子🌰:做飯的步驟
假設你要做一道菜,步驟是:
- 洗鍋 → 2. 熱油 → 3. 放菜 → 4. 翻炒
指令重排后可能變成:
- 洗鍋 → 3. 放菜(未熱油) → 2. 熱油 → 4. 翻炒
→ 單線程下沒問題(最終菜還是熟的),但多線程下可能翻車(其他線程看到“放菜”時油還沒熱)!
為什么需要指令重排?
- 提高執行效率:CPU和編譯器會通過重排指令,充分利用硬件資源(如并行執行不沖突的操作)。
- 減少等待時間:避免因某些操作(如內存讀取延遲)導致的空閑等待。
多線程環境中的問題
指令重排在單線程無感知,但在多線程并發時可能導致意外結果。
經典案例:雙重檢查鎖(DCL)單例模式
public class Singleton {private static Singleton instance;public static Singleton getInstance() {if (instance == null) { // 第一次檢查synchronized (Singleton.class) {if (instance == null) { // 第二次檢查instance = new Singleton(); // 問題在此!}}}return instance;}
}
問題分析:
instance = new Singleton()
實際分為三步:
- 分配內存空間
- 初始化對象
- 將引用指向內存地址
指令重排可能導致步驟2和3顛倒:
→ 其他線程可能在對象未初始化完成時,拿到非空的instance
,導致使用錯誤!
如何禁止指令重排?
-
使用
volatile
關鍵字:
→ 修飾變量(如private volatile static Singleton instance;
),通過插入內存屏障禁止重排序。
→ 解決上述DCL單例問題。 -
使用
synchronized
或Lock
:
→ 同步代碼塊保證原子性和可見性,隱含禁止重排序。
總結
- 指令重排:優化手段,單線程安全,多線程需警惕。
- 解決方案:
volatile
或同步機制確保多線程下的順序一致性。
在 Java 中,volatile
、synchronized
和 Lock
是解決并發問題的三種重要工具。它們有不同的使用場景和特點,下面分別介紹它們的用途、常用方法以及適用場景。
1. volatile
用途
- 保證可見性:確保一個線程對
共享變量的修改
對其他線程立即可見。 - 防止指令重排序:通過插入內存屏障(Memory Barrier)禁止某些編譯器或處理器的指令重排序優化。
特點
- 只適用于單個變量。
- 不保證復合操作(如
x++
)的原子性。 - 開銷較小,性能優于
synchronized
。
常用場景
- 用于狀態標志位(如開關標志)。
- 當只需要保證可見性和有序性時使用。
例子
class VolatileExample {private volatile boolean flag = true;public void stop() {flag = false; // 修改flag,其他線程會立即看到}public void run() {while (flag) {// 執行任務}System.out.println("Thread stopped");}
}
2. synchronized
用途
- 保證互斥性:同一時刻只有一個線程可以執行被同步保護的代碼塊。
- 保證可見性:當一個線程釋放鎖時,會將修改后的變量值刷新到主存中,其他線程獲取鎖時會從主存中讀取最新值。
- 防止指令重排序:通過插入內存屏障確保有序性。
特點
- 可以作用于代碼塊或方法。
- 提供了內置鎖機制,簡單易用。
- 性能較低(相對
volatile
),但在復雜場景下更可靠。
常用方法/用法
-
同步方法:
- 使用
synchronized
修飾方法,鎖定當前對象(即this
)。
public synchronized void increment() {count++; }
- 使用
-
同步代碼塊:
- 使用
synchronized
修飾代碼塊,指定鎖對象。
public void increment() {synchronized (lock) {count++;} }
- 使用
-
靜態同步方法:
- 鎖定的是類對象(
Class
實例)。
public static synchronized void incrementStatic() {staticCount++; }
- 鎖定的是類對象(
例子
class SynchronizedExample {private int count = 0;public synchronized void increment() {count++; // 線程安全}public synchronized int getCount() {return count;}
}
3. Lock(ReentrantLock)
用途
- 提供比
synchronized
更靈活的鎖機制:- 支持公平鎖和非公平鎖。
- 支持嘗試獲取鎖(
tryLock()
)。 - 支持可中斷鎖(
lockInterruptibly()
)。 - 支持超時獲取鎖(
tryLock(long timeout, TimeUnit unit)
)。
- 保證互斥性、可見性和有序性。
特點
- 需要手動加鎖和解鎖(容易忘記解鎖,導致死鎖)。
- 提供更多功能,但使用復雜度更高。
- 性能通常優于
synchronized
(尤其是在高競爭情況下)。
常用方法
-
核心接口:
java.util.concurrent.locks.Lock
void lock()
:獲取鎖,如果鎖不可用則阻塞。void unlock()
:釋放鎖。boolean tryLock()
:嘗試獲取鎖,成功返回true
,失敗返回false
。boolean tryLock(long timeout, TimeUnit unit)
:嘗試在指定時間內獲取鎖。void lockInterruptibly()
:獲取鎖,但可以響應中斷。
-
實現類:
ReentrantLock
- 可重入鎖,支持公平鎖和非公平鎖。
例子
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;class LockExample {private int count = 0;private final Lock lock = new ReentrantLock();public void increment() {lock.lock(); // 加鎖try {count++; // 線程安全} finally {lock.unlock(); // 釋放鎖}}public int getCount() {lock.lock();try {return count;} finally {lock.unlock();}}
}
對比總結
特性 | volatile | synchronized | Lock(ReentrantLock) |
---|---|---|---|
互斥性 | 不支持 | 支持 | 支持 |
可見性 | 支持 | 支持 | 支持 |
有序性 | 支持(禁止重排序) | 支持 | 支持 |
原子性 | 不支持復合操作 | 支持 | 支持 |
靈活性 | 低 | 中 | 高 |
性能 | 高 | 中 | 高(高競爭下優于synchronized ) |
適用場景 | 單個變量的狀態標志 | 方法或代碼塊的同步 | 需要高級功能(如嘗試鎖、公平鎖等) |
選擇建議
- 優先使用
volatile
:- 如果只需要保證可見性和有序性,并且不涉及復合操作。
- 使用
synchronized
:- 如果需要簡單的互斥性、可見性和有序性,且不需要額外的功能。
- 使用
Lock
:- 如果需要更靈活的鎖機制(如嘗試鎖、超時鎖、公平鎖等)。
通過合理選擇工具,可以在保證線程安全的同時,提升程序的性能和可維護性。