一、基本概念
1.1 內(nèi)存模型
在程序的執(zhí)行過(guò)程中,涉及到兩個(gè)方面:指令的執(zhí)行和數(shù)據(jù)的讀寫(xiě)萍肆。其中指令的執(zhí)行通過(guò)處理器來(lái)完成缠导,而數(shù)據(jù)的讀寫(xiě)則要依賴(lài)于系統(tǒng)內(nèi)存廉羔,但是處理器的執(zhí)行速度要遠(yuǎn)大于內(nèi)存數(shù)據(jù)的讀寫(xiě),因此在處理器中加入了高速緩存僻造。在程序的執(zhí)行過(guò)程中憋他,會(huì) 先將數(shù)據(jù)拷貝到處理器的高速緩存中,待運(yùn)算結(jié)束后再回寫(xiě)到系統(tǒng)內(nèi)存當(dāng)中髓削。
在單線(xiàn)程的情況下不會(huì)有什么問(wèn)題竹挡,但是如果在多線(xiàn)程情況下就可能會(huì)出現(xiàn)異常的情況,以下面這段代碼為例立膛,i
是放在堆內(nèi)存的共享變量:
i = i + 1; //i 的初始值為0揪罕。
假如線(xiàn)程A
和線(xiàn)程B
都執(zhí)行這段代碼,那么就可能出現(xiàn)下面兩種情況:
- 第一種情況:線(xiàn)程
A
先執(zhí)行+1
操作宝泵,然后將i
的值寫(xiě)回到系統(tǒng)內(nèi)存中好啰;線(xiàn)程B
從系統(tǒng)內(nèi)存中拷貝i
的值1
到高速緩存中,執(zhí)行完+1
操作再回寫(xiě)到系統(tǒng)內(nèi)存中儿奶,最終的結(jié)果是i=2
框往。 - 第二種情況:線(xiàn)程
A
和線(xiàn)程B
首先都將i
的值0
拷貝到各自處理器的高速緩存當(dāng)中,線(xiàn)程A
首先執(zhí)行+1
操作闯捎,之后i
的值為1
椰弊,然后寫(xiě)回到系統(tǒng)內(nèi)存中;但是對(duì)于線(xiàn)程B
而言瓤鼻,它并不知道這一過(guò)程秉版,在運(yùn)行該線(xiàn)程的處理器的高速緩存中i
的值仍然為0
,因此在它執(zhí)行+1
操作后娱仔,再將i
的值寫(xiě)回到系統(tǒng)內(nèi)存中沐飘,最終的結(jié)果是i=1
。
這種不確定性就稱(chēng)為 緩存不一致牲迫。
1.2 并發(fā)編程中的三個(gè)概念
在并發(fā)編程中耐朴,有三個(gè)關(guān)鍵的概念:可見(jiàn)性、原子性和有序性盹憎,只有保證了這三點(diǎn)才能使得程序在多線(xiàn)程情況下獲得預(yù)期的運(yùn)行結(jié)果筛峭。
1.2.1 可見(jiàn)性
可見(jiàn)性:是指線(xiàn)程之間的可見(jiàn)性,一個(gè)線(xiàn)程修改的狀態(tài)對(duì)另一個(gè)線(xiàn)程是可見(jiàn)的陪每。也就是一個(gè)線(xiàn)程修改的結(jié)果影晓,另一個(gè)線(xiàn)程馬上就能看到镰吵。在1.1
所舉的例子就存在可見(jiàn)性的問(wèn)題。
在Java
中volatile
挂签、synchronized
和final
實(shí)現(xiàn)可見(jiàn)性疤祭。
1.2.2 原子性
原子性:即一個(gè)操作或者多個(gè)操作,要么全部執(zhí)行并且執(zhí)行的過(guò)程不會(huì)被任何因素打斷饵婆,要么就都不執(zhí)行勺馆。
再比如a++
,這個(gè)操作實(shí)際是a=a+1
侨核,是可分割的草穆,所以它不是一個(gè)原子操作。非原子操作都會(huì)存在線(xiàn)程安全問(wèn)題搓译,需要我們使用同步技術(shù)來(lái)讓它變成一個(gè)原子操作悲柱。一個(gè)操作是原子操作,那么我們稱(chēng)它具有原子性些己。
在Java
中synchronized
和在lock
豌鸡、unlock
中操作或者原子操作類(lèi)來(lái)保證原子性。
1.2.3 有序性
有序性:即程序執(zhí)行的順序按照代碼的先后順序執(zhí)行轴总。以下面的代碼為例:
int i = 0;
boolean flag = false;
i = 1; //語(yǔ)句1
flag = true; //語(yǔ)句2
在上面的代碼中定義了一個(gè)整形和Boolean
型變量直颅,并通過(guò)語(yǔ)句1
和語(yǔ)句2
對(duì)這兩個(gè)變量賦值博个,但是JVM
在執(zhí)行這段代碼的時(shí)候并不保證語(yǔ)句1
在語(yǔ)句2
之前執(zhí)行怀樟,也就是說(shuō)可能會(huì)發(fā)生 指令重排序。
指令重排序指的是在 保證程序最終執(zhí)行結(jié)果和代碼順序執(zhí)行的結(jié)果一致的前提 下盆佣,改變語(yǔ)句執(zhí)行的順序來(lái)優(yōu)化輸入代碼往堡,提高程序運(yùn)行效率。
但是這一前提條件在多線(xiàn)程的情況下就有可能出現(xiàn)問(wèn)題共耍,以下面的代碼為例:
//線(xiàn)程1:
context = loadContext(); //語(yǔ)句1
inited = true; //語(yǔ)句2
//線(xiàn)程2:
while (!inited) {
sleep()
}
doSomethingWithConfig(context);
對(duì)于線(xiàn)程1
來(lái)說(shuō)虑灰,語(yǔ)句1
和語(yǔ)句2
沒(méi)有依賴(lài)關(guān)系,因此有可能會(huì)發(fā)生指令重排序的情況痹兜。但是對(duì)于線(xiàn)程2
來(lái)說(shuō)穆咐,語(yǔ)句2
在語(yǔ)句1
之前執(zhí)行,那么就會(huì)導(dǎo)致進(jìn)入doSomethingWithConfig
函數(shù)的時(shí)候context
沒(méi)有初始化字旭。
Java
語(yǔ)言提供了volatile
和synchronized
兩個(gè)關(guān)鍵字來(lái)保證線(xiàn)程之間操作的有序性对湃,volatile
是因?yàn)槠?本身包含禁止指令重排序 的語(yǔ)義,synchronized
是由 一個(gè)變量在同一個(gè)時(shí)刻只允許一條線(xiàn)程對(duì)其進(jìn)行 lock 操作 這條規(guī)則獲得的遗淳,此規(guī)則決定了持有同一個(gè)對(duì)象鎖的兩個(gè)同步塊只能串行執(zhí)行拍柒。
二、volatile 詳解
2.1 定義
volatile
的定義如下:Java
編程語(yǔ)言允許線(xiàn)程訪(fǎng)問(wèn)共享變量屈暗,為了確保共享變量能被準(zhǔn)確和一致地更新拆讯,線(xiàn)程應(yīng)該確保 通過(guò)排它鎖單獨(dú)地獲得這個(gè)變量脂男。如果一個(gè)字段被聲明成volatile
,Java
線(xiàn)程內(nèi)存模型確保 所有線(xiàn)程看到這個(gè)變量的值是一致的种呐。
一旦一個(gè)共享變量被volatile
修飾之后宰翅,那么就具備了兩層語(yǔ)義:
- 保證了不同線(xiàn)程對(duì)這個(gè)變量進(jìn)行操作時(shí)的可見(jiàn)性,即一個(gè)線(xiàn)程修改了某個(gè)變量的值爽室,這新值對(duì)其他線(xiàn)程來(lái)說(shuō)是立即可見(jiàn)的堕油。
- 禁止進(jìn)行指令重排序。
下面肮之,我們用兩個(gè)小結(jié)解釋一下這兩層語(yǔ)義掉缺。
2.2 保證可見(jiàn)性
當(dāng)我們?cè)?code>X86處理器下通過(guò)工具獲取JIT
編譯器生成的匯編指令,來(lái)查看對(duì)volatile
進(jìn)行寫(xiě)操作時(shí)戈擒,會(huì)發(fā)生下面的事情:
//Java 代碼
instance = new Singleton(); //instance 是 volatile 變量
//轉(zhuǎn)變成匯編代碼
0x01a3de1d: move $0 x 0, 0 x 1104800 (%esi);
0x01a3de24: lock add1 $ 0 x 0, (%esp);
有volatile
變量修飾的共享變量 進(jìn)行寫(xiě)操作的時(shí)候 會(huì)多出兩行匯編代碼眶明,Lock
前綴的指令在多核處理器下引發(fā)了兩件事情:
- 將當(dāng)前處理器 內(nèi)部緩存 的數(shù)據(jù)寫(xiě)回到 系統(tǒng)內(nèi)存。
- 這個(gè)寫(xiě)回內(nèi)存的操作會(huì)使在其他處理器里 緩存了該內(nèi)存地址的數(shù)據(jù)無(wú)效筐高,當(dāng)這些處理器對(duì)這個(gè)數(shù)據(jù)進(jìn)行修改操作的時(shí)候搜囱,會(huì)重新從系統(tǒng)內(nèi)存中把數(shù)據(jù)讀到處理器緩存里。
2.3 禁止指令重排序
volatile
關(guān)鍵字禁止指令重排序有兩層意思:
- 當(dāng)程序執(zhí)行到
volatile
變量的讀操作或者寫(xiě)操作時(shí)柑土,在其前面的操作的更改肯定全部已經(jīng)進(jìn)行蜀肘,且結(jié)果已經(jīng)對(duì)后面的操作可見(jiàn);在其后面的操作肯定還沒(méi)有進(jìn)行稽屏; - 在進(jìn)行指令優(yōu)化時(shí)扮宠,不能將在對(duì)
volatile
變量訪(fǎng)問(wèn)的語(yǔ)句放在其后面執(zhí)行,也不能把volatile
變量后面的語(yǔ)句放到其前面執(zhí)行狐榔。
以下面的例子為例:
//flag 為 volatile 變量
x = 2; //語(yǔ)句1
y = 0; //語(yǔ)句2
flag = true; //語(yǔ)句3
x = 4; //語(yǔ)句4
y = -1; //語(yǔ)句5
由于flag
為volatile
變量坛增,因此,可以保證語(yǔ)句1/2
在語(yǔ)句3
之前執(zhí)行薄腻,語(yǔ)句4/5
在其之后執(zhí)行收捣,但是并不保證語(yǔ)句1/2
之間或者語(yǔ)句4/5
之間的順序。
對(duì)于1.2.3
舉的有關(guān)Context
問(wèn)題庵楷,我們就可以通過(guò)將inited
變量聲明為volatile
罢艾,這樣就會(huì)保證loadContext()
和inited
賦值語(yǔ)句之間的順序不被改變,避免出現(xiàn)inited=true
但是Context
沒(méi)有初始化的情況出現(xiàn)尽纽。
2.4 性能問(wèn)題
volatile
相對(duì)于synchronized
的優(yōu)勢(shì)主要原因是兩點(diǎn):簡(jiǎn)易和性能咐蚯。如果從讀寫(xiě)兩方便來(lái)考慮:
-
volatile
讀操作開(kāi)銷(xiāo)非常低,幾乎和非volatile
讀操作一樣 -
volatile
寫(xiě)操作的開(kāi)銷(xiāo)要比非volatile
寫(xiě)操作多很多蜓斧,因?yàn)橐WC可見(jiàn)性需要實(shí)現(xiàn) 內(nèi)存界定仓蛆,即便如此,volatile
的總開(kāi)銷(xiāo)仍然要比鎖獲取低挎春。volatile
操作不會(huì)像鎖一樣 造成阻塞看疙。
以上兩個(gè)條件表明豆拨,可以被寫(xiě)入volatile
變量的這些有效值 獨(dú)立于任何程序的狀態(tài),包括變量的當(dāng)前狀態(tài)能庆。大多數(shù)的編程情形都會(huì)與這兩個(gè)條件的其中之一沖突施禾,使得volatile
不能像synchronized
那樣普遍適用于實(shí)現(xiàn)線(xiàn)程安全。
因此搁胆,在能夠安全使用volatile
的情況下弥搞,volatile
可以提供一些優(yōu)于鎖的可伸縮特性。如果讀操作的次數(shù)要遠(yuǎn)遠(yuǎn)超過(guò)寫(xiě)操作渠旁,與鎖相比攀例,volatile
變量通常能夠減少同步的性能開(kāi)銷(xiāo)。
2.5 應(yīng)用場(chǎng)景
要使volatile
變量提供理想的線(xiàn)程安全顾腊,必須同時(shí)滿(mǎn)足以下兩個(gè)條件:
- 對(duì)變量的 寫(xiě)操作不依賴(lài)于當(dāng)前值粤铭。例如
x++
這樣的增量操作,它實(shí)際上是一個(gè)由讀取杂靶、修改梆惯、寫(xiě)入操作序列組成的組合操作,必須以原子方式執(zhí)行吗垮,而volatile
不能提供必須的原子特性垛吗。 - 該變量 沒(méi)有包含在其它變量的不變式中。
避免濫用volatile
最重要的準(zhǔn)則就是:只有在 狀態(tài)真正獨(dú)立于程序內(nèi)其它內(nèi)容時(shí) 才能使用volatile
烁登,下面怯屉,我們總結(jié)一些volatile
的應(yīng)用場(chǎng)景。
2.5.1 狀態(tài)標(biāo)志
用volatile
來(lái)修飾一個(gè)Boolean
狀態(tài)標(biāo)志防泵,用于指示發(fā)生了某一次的重要事件蚀之,例如完成初始化或者請(qǐng)求停機(jī)蝗敢。
volatile boolean shutdownRequested;
...
public void shutdown() { shutdownRequested = true; }
public void doWork() {
while (!shutdownRequested) {
// do stuff
}
}
2.5.2 一次性安全發(fā)布
在解釋 一次性安全發(fā)布 的含義之前捷泞,讓我們先來(lái)看一下 單例寫(xiě)法 當(dāng)中著名的 雙重檢查鎖定問(wèn)題。
//使用 volatile 修飾寿谴。
private volatile static Singleton sInstance;
public static Singleton getInstance() {
if (sInstance == null) { //(0)
synchronized (Singleton.class) { //(1)
if (sInstance == null) { //(2)
sInstance = new Singleton(); //(3)
}
}
}
return sInstance;
}
假如 沒(méi)有使用volatile
來(lái)修飾sInstance
變量锁右,那么有可能會(huì)發(fā)生下面的場(chǎng)景:
- 第一步:
Thread1
進(jìn)入getInstance()
方法,由于sInstance
為空讶泰,Thread1
進(jìn)入synchronized
代碼塊咏瑟。 - 第二步:
Thread1
前進(jìn)到(3)
處,在構(gòu)造函數(shù)執(zhí)行之前使sInstance
對(duì)象成為非空痪署,并設(shè)置sInstance
指向的內(nèi)存空間码泞。 - 第三步:
Thread2
執(zhí)行,它在入口(0)
處檢查實(shí)例是否為空狼犯,由于sInstance
對(duì)象不為空余寥,Thread2
將sInstance
引用返回领铐,此時(shí)sInstance
對(duì)象并沒(méi)有初始化完成。 - 第四步:
Thread1
通過(guò)運(yùn)行Singleton
對(duì)象的構(gòu)造函數(shù)并將引用返回給它宋舷,來(lái)完成對(duì)該對(duì)象的初始化绪撵。
通過(guò)volatile
就可以禁止第二步和第四步的重排序,也就是使得 初始化對(duì)象在設(shè)置 sInstance 指向的內(nèi)存空間之前完成祝蝠。
2.5.3 volatile bean 模式
volatile bean
模式適用于將JavaBeans
作為“榮譽(yù)結(jié)構(gòu)”使用的框架音诈。在volatile bean
模式中,JavaBean
被用作一組具有getter
和/或setter
方法的獨(dú)立屬性的容器绎狭。
volatile bean
模式的基本原理是:很多框架為易變數(shù)據(jù)的持有者提供了容器细溅,但是放入這些容器中的對(duì)象必須是線(xiàn)程安全的。
在volatile bean
模式中儡嘶,JavaBean
的所有數(shù)據(jù)成員都是volatile
類(lèi)型的谒兄,并且 getter
和setter
方法必須非常普通,除了獲取或設(shè)置相應(yīng)的屬性外社付,不能包含任何邏輯承疲。此外,對(duì)于對(duì)象引用的數(shù)據(jù)成員鸥咖,引用的對(duì)象必須是有效不可變的燕鸽。
public class Person {
private volatile String firstName;
private volatile String lastName;
private volatile int age;
public String getFirstName() { return firstName; }
public String getLastName() { return lastName; }
public int getAge() { return age; }
public void setFirstName(String firstName) {
this.firstName = firstName;
}
public void setLastName(String lastName) {
this.lastName = lastName;
}
public void setAge(int age) {
this.age = age;
}
}
2.5.4 開(kāi)銷(xiāo)較低的讀/寫(xiě)鎖策略
如果讀操作遠(yuǎn)遠(yuǎn)超過(guò)寫(xiě)操作,您可以結(jié)合使用內(nèi)部鎖和volatile
變量來(lái)減少公共代碼路徑的開(kāi)銷(xiāo)啼辣。下面的代碼中使用synchronized
確保增量操作是原子的啊研,并使用volatile
保證當(dāng)前結(jié)果的可見(jiàn)性。如果更新不頻繁的話(huà)鸥拧,該方法可實(shí)現(xiàn)更好的性能党远,因?yàn)樽x路徑的開(kāi)銷(xiāo)僅僅涉及volatile
讀操作,這通常要優(yōu)于一個(gè)無(wú)競(jìng)爭(zhēng)的鎖獲取的開(kāi)銷(xiāo)富弦。
public class CheesyCounter {
private volatile int value;
public int getValue() { return value; }
public synchronized int increment() {
return value++;
}
}
三沟娱、參考文獻(xiàn)
(1) Java 并發(fā)編程:volatile 關(guān)鍵字解析
(2) Java 中 volatile 關(guān)鍵字詳解
(3) 正確使用 volatile 變量
(4) volatile 的使用