volatile 是 java 中一個非常常見神得,功能非常強(qiáng)大的一個關(guān)鍵字肥照,大家用的最多的地方可能就是單例模式的雙重檢查鎖的寫法中败富。提到 volatile 舟山,不得不提 synchronized 兔甘, synchronized 是一個重量級鎖谎碍,那么 volatile 是一個輕量級鎖嗎?并不是洞焙, volatile 是一個輕量級的同步關(guān)鍵字蟆淀,那么 volatile 的語義到底是什么呢?這就是這篇文章要介紹的內(nèi)容澡匪。
從單例模式的雙重檢查鎖寫法說起
首先我們看一下常見的單例模式的雙重檢查鎖寫法熔任。更多單例模式的寫法
public class SingleInstance {
private static SingleInstance sSingleInstance;
private SingleInstance() {
}
public static SingleInstance getInstance() {
if (sSingleInstance == null) {
synchronized (SingleInstance.class) {
if (sSingleInstance == null) {
sSingleInstance = new SingleInstance();
}
}
}
return sSingleInstance;
}
}
上面這種寫法在 《Java 并發(fā)編程實戰(zhàn)》 一書中的評價是【臭名昭著】 ,可見這種寫法是有非常大的問題唁情,不過幸運的是我們有 volatile 關(guān)鍵字疑苔,在 Java 5 以上將 sSingleInstance 用 volatile 關(guān)鍵字修飾就可以解決(具體問題我們下面再討論)。 那么 volatile 為什么能解決問題甸鸟, volatile 又為程序員保證了什么惦费,這就是下面要討論的問題兵迅。
可見性
在 JMM 中,為了提高程序性能薪贫,線程對于變量的讀寫不會直接作用于主存恍箭,而是會先作用于相對應(yīng)的本地內(nèi)存,最后會在合適的時機(jī)再同步到主存后雷。而 volatile 的可見性是指季惯,線程每次在更新本地內(nèi)存的變量之后,會同步刷新到主存中去臀突,同樣的線程每次在讀 volatile 變量時都會將本地內(nèi)存中的值置為無效勉抓。然后線程會直接去主存中讀取相應(yīng)的值 。下面我們用一個例子再加深點認(rèn)識候学。
//程序片段1
class VolatileExample{
int a = 0;
boolean flag = false;
public void write(){
a = 1; //1
flag = true; //2
}
public void read(){
if(flag){
int i = a; //3
... //4
}
}
}
假設(shè) write 方法在 線程 A 中執(zhí)行藕筋, read 方法在線程 B 中執(zhí)行,我們不考慮其他因素(重排序)梳码,假設(shè) A 線程先執(zhí)行隐圾, B 線程后執(zhí)行,那么當(dāng) B 線程執(zhí)行時掰茶, B 線程能否正確讀取到 flag 和 a 的值呢暇藏?很可惜,答案是不一定濒蒋。因為 JVM 不保證何時會將本地內(nèi)存中的值同步到主存中去盐碱。如果我們將程序改成下面這樣,結(jié)果又是如何呢沪伙?
//程序片段2
class VolatileExample{
int a = 0;
volatile boolean flag = false;
public void write(){
a = 1; //1
flag = true; //2
}
public void read(){
if(flag){
int i = a; //3
... //4
}
}
}
同樣的我們暫時不考慮重排序瓮顽,將 flag 使用 volatile 修飾之后, volatile 可以保證在線程 A 修改了 falg 值之后會將 flag 的值同步到主存中去围橡,同樣的在 B 線程讀取 flag 的時候也會去主存中讀取暖混。那么我們還有一個問題,這個時候雖然線程 B 可以正確讀取到 flag 的值翁授,那么線程 B 還能正確讀取到 a 的值嗎拣播?答案是:可以。 volatile 會保證在同步 flag 的值到主存的同時會將寫 volatile 變量之前的操作同時同步到主存中去收擦。同樣的當(dāng)線程 B 開始去主存中讀取 volatile 時贮配,也會去主存中讀取 a 的值。
JMM 的抽象示意圖如下:
阻止重排序
重排序:編譯器和處理器有可能會對不存在數(shù)據(jù)依賴的兩條指令進(jìn)行重排序炬守,這里的數(shù)據(jù)依賴僅指單線程或單個處理器牧嫉。還是以上面的程序片段1為例,重排序是指 1 和 2 以及 3 和 4 的執(zhí)行順序不可預(yù)測『ㄔ澹可能的執(zhí)行順序有:1->2->3->4;2->1->3->4;1->2->4->3;2->1->4->3;
通過將程序片段1改為程序片段2就可以阻止重排序曹洽,最終的執(zhí)行結(jié)果就是:1->2->3->4 ;
為了實現(xiàn) volatile 內(nèi)存語義, JMM 針對編譯器制定的重排序規(guī)則如下:
從上表我們可以看出:
- 當(dāng)?shù)诙€操作是 volatile 寫時辽剧,不管第一個操作是什么送淆,都不會重排序
- 當(dāng)?shù)谝粋€操作是 volatile 讀時,不管第一個操作是什么怕轿,都不會重排序
- 當(dāng)?shù)谝粋€操作是 volatile 寫偷崩,第二個操作是 volatile 讀是不會重排序
再探雙重檢查鎖
下面我們再來看看為什么下面這種雙重檢查鎖寫法就不是【臭名昭著】的了呢?更多單例模式的寫法
public class SingleInstance {
private static volatile SingleInstance sSingleInstance;
private SingleInstance() {
}
public static SingleInstance getInstance() {
if (sSingleInstance == null) {
synchronized (SingleInstance.class) {
if (sSingleInstance == null) {
sSingleInstance = new SingleInstance();
}
}
}
return sSingleInstance;
}
}
首先撞羽,我們要討論的是開頭的那種寫法存在的問題阐斜。
其實 sSingleInstance = new SingleInstance(); 這句代碼看起來只有一句,但是被編譯成指令是3句诀紊,分別是
- 為 SingleInstance 分配內(nèi)存空間
- 調(diào)用 SingleInstance 的構(gòu)造函數(shù)谒出,初始化成員變量
- 為 sSingleInstance 賦值
根據(jù)前面的知識我們知道第 2 步和第 3 步可能會重排序,這樣的話有可能某個線程獲取到的單例就是未完全初始化的實例邻奠,為了解決這個問題笤喳,我們用 volatile 修飾 sSingleInstance 之后,根據(jù)上面的阻止重排序規(guī)則我們知道 volatile 寫和前面的一條指令不會進(jìn)行重排序碌宴,所以也就不會有問題了杀狡,這就是 volatile 的妙用。其實 volatile 遠(yuǎn)不止這點用處贰镣,在 Java 提供的并發(fā)包中有很多工具類的實現(xiàn)基礎(chǔ)就是 volatile 呜象,這些大家可以進(jìn)一步了解。