還記得上一篇文章當(dāng)中提到的內(nèi)存屏障(Memory Fence)嗎记劝?其實(shí)Volatile的實(shí)現(xiàn)原理就是通過內(nèi)存屏障來實(shí)現(xiàn)的想许。
-
對(duì)于volatile修飾的變量:
- 在該變量的寫指令后,會(huì)加入寫屏障
- 在該變量的讀指令前膊毁,會(huì)加入讀屏障
上面先放個(gè)結(jié)論丰歌,后面我們逐步的看它是什么意思惰帽。
我們看下有如下的代碼,主要是為了理解寫屏障和讀屏障是如何添加召噩,且填在的位置在何處:
public class VolatileTest {
/**
* 定義一個(gè)volatile修飾的共享變量
*/
volatile static boolean flag = false;
/**
* 定義全局變量num
*/
static int num = 0;
public static void test1() {
num = 2;
// 此處修改數(shù)據(jù)ready母赵,會(huì)增加一個(gè)寫屏障,從而num具滴、ready在修改數(shù)據(jù)后凹嘲,都會(huì)添加到主存當(dāng)中
flag = true;
}
public static void test2() {
// 此處讀取數(shù)據(jù)ready,會(huì)增加一個(gè)讀屏障抵蚊,保證后面的ready和num都會(huì)從主存當(dāng)中獲取數(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í)行谷醉,會(huì)有如下過程:
t1執(zhí)行test1方法,此時(shí)將num賦值稱為2冈闭,num此時(shí)可能沒有推送到主存當(dāng)中俱尼。之后又執(zhí)行了對(duì)flag賦值的操作,因?yàn)閒lag是volatile修飾的萎攒,所以一定會(huì)將flag更新到主存遇八,同時(shí)將num也會(huì)更新到主存。
t2執(zhí)行test2方法時(shí)耍休,首先會(huì)讀取flag的值刃永,由于flag是有volatile修飾,此時(shí)會(huì)從主存拉取flag的值羊精,同時(shí)num也會(huì)從主存獲取斯够。
一、可見性如何保證喧锦?
前文說到读规,寫屏障對(duì)于共享變量的所有修改,在寫屏障前的所有共享變量燃少,都需要同步到主內(nèi)存當(dāng)中束亏。
讀屏障對(duì)于共享變量的所有修改,在讀屏障后的所有共享變量阵具,都需要同從主存當(dāng)中獲取碍遍。
在文章開始的例子當(dāng)中已經(jīng)闡述了流程:
在修改flag的值時(shí)定铜,所依靠的是寫屏障,會(huì)在flag被修改后的位置添加一個(gè)寫屏障雀久,在寫屏障之前的的num宿稀、和flag修改后的值都會(huì)同步到主存當(dāng)中趁舀。
在讀取flag的值時(shí)赖捌,所依靠的是讀屏障,在flag讀取之前增加一份讀屏障矮烹,在讀屏障后讀取的flag和num都會(huì)從主存當(dāng)中獲取越庇。
二、有序性如何保證奉狈?
寫屏障保證在發(fā)生指令重排序時(shí)卤唉,不會(huì)將寫屏障之前的代碼放在寫屏障之后。
讀屏障會(huì)確保指令重排序時(shí)仁期,不會(huì)將讀屏障后的代碼放在讀屏障之前桑驱。
假設(shè)在volatile關(guān)鍵字之前有多個(gè)變量被修改的語句,那么volatile是不能保證其執(zhí)行的順序跛蛋,能保證的僅僅是在寫屏障前的所有代碼都執(zhí)行完畢熬的,并且寫屏障前的修改對(duì)于讀屏障后代碼一定是可見的。
假如讀取在寫屏障之前赊级,那么則不能保證了押框。
另外需要注意的是,有序性只保證在當(dāng)前線程內(nèi)的代碼不被重排序理逊。
三橡伞、happens-before原則
happens-before 規(guī)定了對(duì)共享變量的寫操作對(duì)其它線程的讀操作可見,可以說它是可見性與有序性的一套規(guī)則總結(jié)晋被。
JMM(java memory model兑徘,java內(nèi)存模型)在以下的情況可以保證,線程對(duì)共享變量的寫羡洛,對(duì)于其他線程是讀可見的挂脑,最常見的有以下兩種:
-
使用synchronized關(guān)鍵字
前面的文章提到過,當(dāng)使用重量級(jí)鎖時(shí)翘县,對(duì)于共享變量的修改時(shí)要同步到主存的最域。
使用volatile修飾的共享變量
還有以下場(chǎng)景(更多的不在下面舉例了):
當(dāng)線程修改共享變量的值,其結(jié)束后锈麸,其他線程對(duì)于修改后的值是可見的镀脂。
線程start()之前,對(duì)于變量修改后的值忘伞,對(duì)其是可見的薄翅。
線程t1修改變量的值沙兰,隨后對(duì)正在讀取該變量的t2進(jìn)行打斷,此時(shí)t1打斷線程t2翘魄,則t2對(duì)于修改后的變量讀可見鼎天。
四、Double-Checked Locking
相信同學(xué)們都學(xué)習(xí)過單例模式暑竟,應(yīng)該都知道其有很多種實(shí)現(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)鍵字去修飾實(shí)例變量instance但荤,那么是為什么呢罗岖?
我們先假設(shè)沒有加volatile關(guān)鍵字的情況,這種情況下砸多線程情況下是會(huì)存在問題的腹躁。
如下所示桑包,是在沒有添加volatile關(guān)鍵字時(shí)的字節(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)建一個(gè)完整的對(duì)象實(shí)例需要兩個(gè)步驟:
實(shí)例化一個(gè)對(duì)象纺非,即new 出來的對(duì)象哑了,此時(shí)是一個(gè)默認(rèn)的空對(duì)象,其屬性等并沒有賦值烧颖,只是創(chuàng)建了引用弱左,我們可以認(rèn)為此時(shí)是一個(gè)半初始化對(duì)象。
初始化步驟倒信,此時(shí)需要去調(diào)用對(duì)象的構(gòu)造方法科贬,完成屬性的賦值等操作,只有經(jīng)過此步驟才是一個(gè)完成的對(duì)象鳖悠。
對(duì)應(yīng)到上面的字節(jié)碼文件榜掌,分別是以下的代碼:
- 17:創(chuàng)建一個(gè)引用,將引用入棧
- 20:復(fù)制地址引用乘综,用于后面使用
- 21:通過前面復(fù)制的地址引用憎账,調(diào)用對(duì)象的構(gòu)造方法
- 24:將引用賦值到靜態(tài)變量instance上
相信同學(xué)們應(yīng)該能夠?qū)?yīng)的上的。
在jvm中呢卡辰,如果完全按照上面的步驟執(zhí)行則不會(huì)有問題胞皱,但是jvm會(huì)優(yōu)化為先執(zhí)行24步驟,再執(zhí)行21步驟九妈,那么結(jié)果可想而知反砌,此時(shí)靜態(tài)變量是一個(gè)半初始化的對(duì)象萌朱。
當(dāng)另外的線程來執(zhí)行g(shù)etInstance方法時(shí),獲取靜態(tài)實(shí)例對(duì)象instance晶疼,即字節(jié)碼文件的第0行酒贬,此行代碼是在鎖synchronized(管程monitorenter)之外,誰來都可以執(zhí)行锭吨,那么獲取到了就是半初始對(duì)象,不是null零如,那么一定是有問題的。
通過我們前面的學(xué)習(xí)埠况,就可以用volatile來解決DCL的這個(gè)問題:
這個(gè)volatile關(guān)鍵字在字節(jié)碼是體現(xiàn)不出來的耸携,但是手動(dòng)標(biāo)記一下它的位置辕翰,只保留主要位置:
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的順序不變啊喜命,因?yàn)槎际窃趯懫琳现埃荒鼙WC寫屏障之前的代碼不會(huì)被放到寫屏障后河劝。那么它是如何解決的呢壁榕?
其實(shí)在更加底層volatile轉(zhuǎn)成匯編語言,是在該代碼上增加了lock前綴赎瞎,此時(shí)會(huì)將其之前的代碼鎖住牌里,直到執(zhí)行到這個(gè)lock,此時(shí)前面的代碼都一定執(zhí)行完了务甥。
從根本說volatile的實(shí)現(xiàn)是是一條CPU原語 lock addl牡辽。
太過底層就不多贅述了,畢竟我也沒學(xué)到位呢3佟L痢!挺尿!