還記得上一篇文章當中提到的內(nèi)存屏障(Memory Fence)嗎?其實Volatile的實現(xiàn)原理就是通過內(nèi)存屏障來實現(xiàn)的。
-
對于volatile修飾的變量:
- 在該變量的寫指令后,會加入寫屏障
- 在該變量的讀指令前,會加入讀屏障
上面先放個結(jié)論,后面我們逐步的看它是什么意思。
我們看下有如下的代碼,主要是為了理解寫屏障和讀屏障是如何添加,且填在的位置在何處:
public class VolatileTest {
/**
* 定義一個volatile修飾的共享變量
*/
volatile static boolean flag = false;
/**
* 定義全局變量num
*/
static int num = 0;
public static void test1() {
num = 2;
// 此處修改數(shù)據(jù)ready,會增加一個寫屏障,從而num、ready在修改數(shù)據(jù)后,都會添加到主存當中
flag = true;
}
public static void test2() {
// 此處讀取數(shù)據(jù)ready,會增加一個讀屏障,保證后面的ready和num都會從主存當中獲取數(shù)據(jù)
if (flag) {
System.out.println(num);
}
}
public static void main(String[] args) {
new Thread(() -> {
test1();
}, "t1").start();
new Thread(() -> {
test2();
}, "t2").start();
}
}
如上所示,有volatile修飾的變量flag,假設(shè)上述代碼t1先執(zhí)行,t2后執(zhí)行,會有如下過程:
t1執(zhí)行test1方法,此時將num賦值稱為2,num此時可能沒有推送到主存當中。之后又執(zhí)行了對flag賦值的操作,因為flag是volatile修飾的,所以一定會將flag更新到主存,同時將num也會更新到主存。
t2執(zhí)行test2方法時,首先會讀取flag的值,由于flag是有volatile修飾,此時會從主存拉取flag的值,同時num也會從主存獲取。
一、可見性如何保證?
前文說到,寫屏障對于共享變量的所有修改,在寫屏障前的所有共享變量,都需要同步到主內(nèi)存當中。
讀屏障對于共享變量的所有修改,在讀屏障后的所有共享變量,都需要同從主存當中獲取。
在文章開始的例子當中已經(jīng)闡述了流程:
在修改flag的值時,所依靠的是寫屏障,會在flag被修改后的位置添加一個寫屏障,在寫屏障之前的的num、和flag修改后的值都會同步到主存當中。
在讀取flag的值時,所依靠的是讀屏障,在flag讀取之前增加一份讀屏障,在讀屏障后讀取的flag和num都會從主存當中獲取。
二、有序性如何保證?
寫屏障保證在發(fā)生指令重排序時,不會將寫屏障之前的代碼放在寫屏障之后。
讀屏障會確保指令重排序時,不會將讀屏障后的代碼放在讀屏障之前。
假設(shè)在volatile關(guān)鍵字之前有多個變量被修改的語句,那么volatile是不能保證其執(zhí)行的順序,能保證的僅僅是在寫屏障前的所有代碼都執(zhí)行完畢,并且寫屏障前的修改對于讀屏障后代碼一定是可見的。
假如讀取在寫屏障之前,那么則不能保證了。
另外需要注意的是,有序性只保證在當前線程內(nèi)的代碼不被重排序。
三、happens-before原則
happens-before 規(guī)定了對共享變量的寫操作對其它線程的讀操作可見,可以說它是可見性與有序性的一套規(guī)則總結(jié)。
JMM(java memory model,java內(nèi)存模型)在以下的情況可以保證,線程對共享變量的寫,對于其他線程是讀可見的,最常見的有以下兩種:
-
使用synchronized關(guān)鍵字
前面的文章提到過,當使用重量級鎖時,對于共享變量的修改時要同步到主存的。
使用volatile修飾的共享變量
還有以下場景(更多的不在下面舉例了):
當線程修改共享變量的值,其結(jié)束后,其他線程對于修改后的值是可見的。
線程start()之前,對于變量修改后的值,對其是可見的。
線程t1修改變量的值,隨后對正在讀取該變量的t2進行打斷,此時t1打斷線程t2,則t2對于修改后的變量讀可見。
四、Double-Checked Locking
相信同學(xué)們都學(xué)習(xí)過單例模式,應(yīng)該都知道其有很多種實現(xiàn)方式,其中有一種就是double-checked locking(雙重檢查鎖)的方式,如下所示:
public class Singleton {
/**
* volatile 解決指令重排序?qū)е碌膯栴}
*/
private static volatile Singleton instance;
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
private Singleton() {
}
}
通過我們的嘗試知道DCL一定要加上volatile關(guān)鍵字去修飾實例變量instance,那么是為什么呢?
我們先假設(shè)沒有加volatile關(guān)鍵字的情況,這種情況下砸多線程情況下是會存在問題的。
如下所示,是在沒有添加volatile關(guān)鍵字時的字節(jié)碼文件:
public class com.cloud.bssp.designpatterns.singleton.lazy.dcl.Singleton {
public static com.cloud.bssp.designpatterns.singleton.lazy.dcl.Singleton getInstance();
Code:
0: getstatic #1 // Field instance:Lcom/cloud/bssp/designpatterns/singleton/lazy/dcl/Singleton;
3: ifnonnull 37
6: ldc #2 // class com/cloud/bssp/designpatterns/singleton/lazy/dcl/Singleton
8: dup
9: astore_0
10: monitorenter
11: getstatic #1 // Field instance:Lcom/cloud/bssp/designpatterns/singleton/lazy/dcl/Singleton;
14: ifnonnull 27
17: new #2 // class com/cloud/bssp/designpatterns/singleton/lazy/dcl/Singleton
20: dup
21: invokespecial #3 // Method "<init>":()V
24: putstatic #1 // Field instance:Lcom/cloud/bssp/designpatterns/singleton/lazy/dcl/Singleton;
27: aload_0
28: monitorexit
29: goto 37
32: astore_1
33: aload_0
34: monitorexit
35: aload_1
36: athrow
37: getstatic #1 // Field instance:Lcom/cloud/bssp/designpatterns/singleton/lazy/dcl/Singleton;
40: areturn
Exception table:
from to target type
11 29 32 any
32 35 32 any
}
我們需要了解的是,jvm創(chuàng)建一個完整的對象實例需要兩個步驟:
實例化一個對象,即new 出來的對象,此時是一個默認的空對象,其屬性等并沒有賦值,只是創(chuàng)建了引用,我們可以認為此時是一個半初始化對象。
初始化步驟,此時需要去調(diào)用對象的構(gòu)造方法,完成屬性的賦值等操作,只有經(jīng)過此步驟才是一個完成的對象。
對應(yīng)到上面的字節(jié)碼文件,分別是以下的代碼:
- 17:創(chuàng)建一個引用,將引用入棧
- 20:復(fù)制地址引用,用于后面使用
- 21:通過前面復(fù)制的地址引用,調(diào)用對象的構(gòu)造方法
- 24:將引用賦值到靜態(tài)變量instance上
相信同學(xué)們應(yīng)該能夠?qū)?yīng)的上的。
在jvm中呢,如果完全按照上面的步驟執(zhí)行則不會有問題,但是jvm會優(yōu)化為先執(zhí)行24步驟,再執(zhí)行21步驟,那么結(jié)果可想而知,此時靜態(tài)變量是一個半初始化的對象。
當另外的線程來執(zhí)行g(shù)etInstance方法時,獲取靜態(tài)實例對象instance,即字節(jié)碼文件的第0行,此行代碼是在鎖synchronized(管程monitorenter)之外,誰來都可以執(zhí)行,那么獲取到了就是半初始對象,不是null,那么一定是有問題的。
通過我們前面的學(xué)習(xí),就可以用volatile來解決DCL的這個問題:
這個volatile關(guān)鍵字在字節(jié)碼是體現(xiàn)不出來的,但是手動標記一下它的位置,只保留主要位置:
0: getstatic #1 // Field instance:Lcom/cloud/bssp/designpatterns/singleton/lazy/dcl/Singleton;
--------------------- 此處加入讀屏障 --------------------
3: ifnonnull 37
6: ldc #2 // class com/cloud/bssp/designpatterns/singleton/lazy/dcl/Singleton
8: dup
9: astore_0
10: monitorenter
11: getstatic #1 // Field instance:Lcom/cloud/bssp/designpatterns/singleton/lazy/dcl/Singleton;
14: ifnonnull 27
17: new #2 // class com/cloud/bssp/designpatterns/singleton/lazy/dcl/Singleton
20: dup
21: invokespecial #3 // Method "<init>":()V
24: putstatic #1 // Field instance:Lcom/cloud/bssp/designpatterns/singleton/lazy/dcl/Singleton;
--------------------- 此處加入寫屏障 --------------------
27: aload_0
28: monitorexit
但是根據(jù)我們前面學(xué)習(xí)的,寫屏障似乎并不能保證21和24的順序不變啊,因為都是在寫屏障之前,它只能保證寫屏障之前的代碼不會被放到寫屏障后。那么它是如何解決的呢?
其實在更加底層volatile轉(zhuǎn)成匯編語言,是在該代碼上增加了lock前綴,此時會將其之前的代碼鎖住,直到執(zhí)行到這個lock,此時前面的代碼都一定執(zhí)行完了。
從根本說volatile的實現(xiàn)是是一條CPU原語 lock addl。
太過底層就不多贅述了,畢竟我也沒學(xué)到位呢?。。?!