內存屏障(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;
}
}
如何保證有序性?
- 寫屏障在指令重排序時,確保不會將寫屏障之前代碼排到寫屏障之後
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 按如下時間序列執行:
- 關鍵在於 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 之間的可見性與有序性