java volatile關鍵字內存原理

內存屏障(Memory Barrier)

1.可見性

  • 寫屏障(Sfence)保證該屏障之前的,對共享變量改動都同步到主內存中去
  • 讀屏障(Ifence)保證該屏障之後的,對共享變量讀取加載的爲主內存中最新數據

2.有序性

  • 寫屏障在指令重排序時,不會將寫屏障之前的代碼排到屏障之後
  • 讀屏障在指令重排序時,不會將讀屏障之後的代碼排到屏障之前

volatile原理

volatile底層原理基於內存屏障

  • 對volatile變量寫指令會在之後加入寫屏障
  • 對volatile變量讀指令會在之前加入讀屏障

如何保證可見性?

  • 寫屏障(sfence)保證在該屏障之前的,對共享變量的改動,都同步到主存當中
public void actor2(I_Result r) {
 num = 2;
 ready = true; // ready 是 volatile 賦值帶寫屏障
 // 寫屏障
}
  • 而讀屏障(lfence)保證在該屏障之後,對共享變量的讀取,加載的是主存中最新數據
public void actor1(I_Result r) {
 // 讀屏障
 // ready 是 volatile 讀取值帶讀屏障
 if(ready) {
 r.r1 = num + num;
 } else {
 r.r1 = 1;
 }
}
T1線程numvolatile ready=falseT2num=2寫屏障ready = truenum=2讀屏障ready=trueT1線程numvolatile ready=falseT2

如何保證有序性?

  • 寫屏障在指令重排序時,確保不會將寫屏障之前代碼排到寫屏障之後
public void actor2(I_Result r) {
 num = 2;
 ready = true; // ready 是 volatile 賦值帶寫屏障
 // 寫屏障
}
  • 寫屏障在指令重排序時,確保不會將讀屏障之後代碼排到讀屏障之前
public void actor1(I_Result r) {
 // 讀屏障
 // ready 是 volatile 讀取值帶讀屏障
 if(ready) {
 r.r1 = num + num;
 } else {
 r.r1 = 1;
 }
}

不能解決指令交錯:
寫屏障僅僅是保證之後的讀能夠讀到最新的結果,但不能保證讀跑到它前面去
而有序性的保證也只是保證了本線程內相關代碼不被重排序


double-checking lock問題

  • 方式一
public class Singleton {
    // 私有化構造
    private void Singleton(){}
    private static Singleton singleton = null;
    //在靜態方法上添加synchronized鎖住的事類對象
    public static synchronized Singleton getInstance(){
       if(singleton == null){
           singleton = new Singleton();
       }
       return singleton;
    }
    //與getInstance相同
    public static  Singleton getInstance1(){
        synchronized(Singleton.class){
            if(singleton == null){
                singleton = new Singleton();
            }
        }
        return singleton;
    }
}

上述代碼本質上是沒有問題,但是在性能方面不太優化。因爲多線程情況下,每次調用getInstance方法,都要進行判斷並且都要獲取鎖。


  • 方式二
public class Singleton {
    // 私有化構造
    private void Singleton(){}
    private static Singleton singleton = null;
 
    //解決了如果已經存在實例對象無須進行加解鎖操作,提高性
    public static  Singleton getInstance(){
        if(singleton == null){
            // 首次訪問會同步,而之後的使用沒有 synchronized
            synchronized(Singleton.class){
                if(singleton == null){
                    singleton = new Singleton();
                }
            }
        }
        return singleton;
    }
}

特點:

  • 懶惰實例化
  • 首次使用 getInstance() 才使用 synchronized 加鎖,後續使用時無需加鎖
    有隱含的,但很關鍵的一點:第一個 if 使用了 INSTANCE 變量,是在同步塊之外
    但在多線程環境下,上面的代碼是有問題的,getInstance 方法對應的字節碼爲:
0: getstatic #2 // 獲取靜態變量Instance Field INSTANCE:com/single/Singleton; 
3: ifnonnull 37 // 判斷不是null 跳轉到37行
6: ldc #3 // 獲取類對象鎖 class com/single/Singleton
8: dup
9: astore_0
10: monitorenter
11: getstatic #2 // Field INSTANCE:com/single/Singleton;
14: ifnonnull 27
17: new #3 //創建實例 class com/single/Singleton
20: dup    //複製引用
21: invokespecial #4 // 複製的引用調用構造方法Method "<init>":()V
24: putstatic #2 // 賦值 Field INSTANCE:com/single/Singleton;
27: aload_0
28: monitorexit
29: goto 37
32: astore_1
33: aload_0
34: monitorexit
35: aload_1
36: athrow
37: getstatic #2 // Field INSTANCE:com/single/Singleton;
40: areturn

其中

  • 17 表示創建對象,將對象引用入棧 // new Singleton
  • 20 表示複製一份對象引用 // 引用地址
  • 21 表示利用一個對象引用,調用構造方法
  • 24 表示利用一個對象引用,賦值給 static INSTANCE
    也許 jvm 會優化爲:先執行 24,再執行 21。如果兩個線程 t1,t2 按如下時間序列執行:
T1T2INSTANCE17:new Singleton20:dup21:putstatic 對象引用賦值0:獲取靜態變量Instance3: ifnonnull 37 // 判斷不是null 跳轉到37行37: getstatic 獲取變量40: areturn使用對象21:nvokespecialT1T2INSTANCE
  • 關鍵在於 0: getstatic 這行代碼在 monitor 控制之外,一些線程可以越過 monitor 讀取
    INSTANCE 變量的值
  • 這時 t1 還未完全將構造方法執行完畢,如果在構造方法中要執行很多初始化操作,那麼 t2 拿到的是將是一個未初始化完畢的單例
  • 對 INSTANCE 使用 volatile 修飾即可,可以禁用指令重排,但要注意在 JDK 5 以上的版本的 volatile 纔會真正有效

double-checking lock解決

public class Singleton {
    // 私有化構造
    private void Singleton(){}
    private static volatile Singleton singleton = null;
 
    //解決了如果已經存在實例對象無須進行加解鎖操作,提高性
    public static  Singleton getInstance(){
        if(singleton == null){
            // 首次訪問會同步,而之後的使用沒有 synchronized
            synchronized(Singleton.class){
                if(singleton == null){
                    singleton = new Singleton();
                }
            }
        }
        return singleton;
    }
}

字節碼上看不出volatile執行

// -------------------------------------> 加入對 INSTANCE 變量的讀屏障
0: getstatic #2 // Field INSTANCE:com/single/Singleton;
3: ifnonnull 37
6: ldc #3 // class com/single/Singleton
8: dup
9: astore_0
10: monitorenter -----------------------> 保證原子性、可見性
11: getstatic #2 // Field INSTANCE:com/single/Singleton;
14: ifnonnull 27
17: new #3 // class com/single/Singleton
20: dup
21: invokespecial #4 // Method "<init>":()V
24: putstatic #2 // Field INSTANCE:com/single/Singleton;
// -------------------------------------> 加入對 INSTANCE 變量的寫屏障
27: aload_0
28: monitorexit ------------------------> 保證原子性、可見性
29: goto 37
32: astore_1
33: aload_0
34: monitorexit
35: aload_1
36: athrow
37: getstatic #2 // Field INSTANCE:com/single/Singleton;
40: areturn

如上面的註釋內容所示,讀寫 volatile 變量時會加入內存屏障(Memory Barrier(Memory Fence)),保證下面
兩點:

可見性

  • 寫屏障(sfence)保證在該屏障之前的 t1 對共享變量的改動,都同步到主存當中
  • 而讀屏障(lfence)保證在該屏障之後 t2 對共享變量的讀取,加載的是主存中最新數據

有序性

  • 寫屏障會確保指令重排序時,不會將寫屏障之前的代碼排在寫屏障之後
  • 讀屏障會確保指令重排序時,不會將讀屏障之後的代碼排在讀屏障之前
  • 更底層是讀寫變量時使用 lock 指令來多核 CPU 之間的可見性與有序性
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章