原文鏈接:https://tech.meituan.com/java-memory-reordering.html
本文提煉核心觀點,輔以圖解與代碼示例,幫你區分 **指令重排** 與 **內存可見性** 兩大根本問題,並指導在實際工程中如何在 volatile、Unsafe.putOrderedXxx()、synchronized、final 之間做最優選擇。
| 問題 | 本質 | 典型後果 | 解決手段 |
|---|---|---|---|
| 指令重排 | 編譯器/CPU 為優化性能,調整指令執行順序 | 單線程語義不變,多線程下邏輯錯誤(如 DCL 失效) | volatile、synchronized、final、Unsafe.putOrderedXxx |
| 內存可見性(緩存不一致) | 多核 CPU 各自 L1/L2 緩存,寫入未及時刷回主存 | 線程 A 寫入,線程 B 讀到舊值 | volatile、synchronized、Lock、Unsafe.putXxxVolatile |
關鍵結論:volatile **同時禁止指令重排 + 保證可見性**(強語義);Unsafe.putOrderedXxx() **僅禁止指令重排,不保證即時可見**(弱語義,性能更優)。
StoreStore、寫入後插入 StoreLoad;讀取前插入 LoadLoad、讀取後插入 LoadStore;Lock 前綴指令觸發總線鎖定/緩存鎖定,強制寫入主存並使其他核緩存行失效;StoreLoad 屏障開銷大(約 10-100ns),高頻寫入場景成為瓶頸。// 僅插入 StoreStore 屏障,無 StoreLoad,不強制刷主存 public native void putOrderedObject(Object o, long offset, Object x); public native void putOrderedLong (Object o, long offset, long x); public native void putOrderedInt (Object o, long offset, int x);
AtomicLong.lazySet()、LongAdder 內部計數器更新、環形隊列生產者寫入序列號。synchronized / Lock / 原子類;volatile;Unsafe.putOrderedXxx() / AtomicXxx.lazySet();final(構造函數結束前的寫入不會重排到構造後,且對其他線程可見)。public class Singleton {
private volatile static Singleton instance; // 關鍵 volatile
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) instance = new Singleton(); // 非原子操作:分配內存→初始化→賦值引用
}
}
return instance;
}
}
無 volatile 時,步驟 2、3 可能重排為「分配→賦值引用→初始化」,導致另一線程看到非 null 但未初始化完成的對象。volatile 禁止此重排並保證可見性。
指令重排與內存可見性是並發編程的兩大基石問題。volatile 是「重型武器」同時解決兩者;Unsafe.putOrderedXxx() 是「輕型武器」僅解決有序性,性能優勢明顯。理解底層內存屏障與 MESI 協議,能讓你在高併發熱點路徑(如計數器、隊列、發布訂閱)做出**最小必要同步**的精準選擇,榨乾多核 CPU 性能。