- Java 的 volatile關(guān)鍵字對(duì)可見性的保證
- Java 的 volatile關(guān)鍵字在保證可見性之前的所做的事情
- 為什么volatile關(guān)鍵字有時(shí)候也不是足夠的
- 什么時(shí)候volatile足夠了
- volatile關(guān)鍵字對(duì)效率的影響
Java關(guān)鍵字用于將一個(gè)變量標(biāo)記為“存儲(chǔ)在內(nèi)存中的變量”。更準(zhǔn)確的說,意思就是每一次對(duì)volatile標(biāo)記的變量進(jìn)行讀取的時(shí)候,都是直接從電腦的主內(nèi)存進(jìn)行的,而不是從cpu的cache中,而且每個(gè)對(duì)volatile變量的寫入操作,都會(huì)被直接寫入到主存里,而不是只寫到cache里。
實(shí)際上,從java5開始,volatile關(guān)鍵字就不僅僅是保證volatile變量從主存讀寫,筆者會(huì)在后面詳細(xì)討論這個(gè)問題。
Java 的 volatile關(guān)鍵字對(duì)可見性的保證
Java的volatile關(guān)鍵字可以保證變量的可見性。說起來很簡(jiǎn)單,但具體是什么意思呢?
在多線程的應(yīng)用程序中,線程操作非volatile的變量,為了更快速的執(zhí)行程序,每個(gè)線程都會(huì)將變量從主存復(fù)制到cpu的cache中。如果你的電腦有多個(gè)cpu,每個(gè)線程都在不同的cpu上運(yùn)行,這就意味著,每個(gè)線程將變量的值復(fù)制到不同的cpu的cache上,就像下面這個(gè)圖所表明:

如果變量沒有聲明為volatile,那么就無法知道,變量什么時(shí)候從主存中讀取到cpu的cache中,有什么時(shí)候從cache中寫回到主存中。這就可能造成很多潛在的問題:
假設(shè)一種情況,多個(gè)線程同時(shí)持有一個(gè)共享對(duì)象的引用,這個(gè)對(duì)象包括一個(gè)counter變量:
public class SharedObject {
public int counter = 0;
}
假設(shè)這種情況,只有線程1自增了這個(gè)counter變量,但是線程1和線程2可能隨時(shí)讀取這個(gè)counter變量。如果這個(gè)counter變量沒有被聲明為volatile,那么就無法確認(rèn),什么時(shí)候counter的變量的值會(huì)從cpu的cache中寫回到主存中,這就意味著,counter變量的值在cpu的cache中的值可能和主存中不一樣,如下圖所示:

這個(gè)線程的問題無法及時(shí)的看到變量的最新的值,因?yàn)榭赡苓@個(gè)變量還沒有被另一個(gè)線程寫回到主存中。所以一個(gè)線程對(duì)一個(gè)變量的更新對(duì)其他的線程是不可見的。這就是我們最初提出的線程的可見性問題。
通過將一個(gè)變量聲明為volatile,那么所有對(duì)這個(gè)變量寫操作會(huì)被直接寫回到主內(nèi)存中,所以這對(duì)線程都是可見的。而且,所有對(duì)這個(gè)變量的讀取操作,也會(huì)直接從主存中讀取,下面說明了如何聲明一個(gè)voaltile變量:
public class SharedObject {
public volatile int counter = 0;
}
** 將一個(gè)變量聲明為volatile就可以保證寫操作,其他線程對(duì)這個(gè)變量的可見性 **
Java 的 volatile關(guān)鍵字在保證可見性之前的所做的事情
從java5開始,volatile關(guān)鍵字不僅可以保證變量直接從主內(nèi)存中讀取,還有一下作用:
- 如果線程A對(duì)一個(gè)volatile變量進(jìn)行寫操作,線程B隨后讀取同一個(gè)volatile值,那么在線程將變量寫操作完成之后的所有變量對(duì)線程A和B都是可見的。
- 那些操作volatile變量的讀寫指令的順序無法被JVM改變(JVM有時(shí)候?yàn)榱诵蕰?huì)改變變量讀寫順序,只要JVM判斷改變順序?qū)Τ绦驔]有影響的話)。
上面兩段話不是很理解,我們接下來進(jìn)行一個(gè)更細(xì)致的說明:
當(dāng)一個(gè)線程對(duì)一個(gè)volatile變量進(jìn)行寫操作的時(shí)候,不僅僅是這個(gè)變量自己被寫入到主存中,同時(shí),其他所有在這之前被改變值的變量也都會(huì)線程先寫入到主存中。
當(dāng)一個(gè)線程對(duì)一個(gè)volatile變量進(jìn)行讀取操作,他也會(huì)將所有跟著那個(gè)volatile變量一起寫入到主存中的其他所有變量一起讀出來。
看下面這個(gè)例子:
Thread A:
sharedObject.nonVolatile = 123;
sharedObject.counter = sharedObject.counter + 1;
Thread B:
int counter = sharedObject.counter;
int nonVolatile = sharedObject.nonVolatile;
因?yàn)榫€程A在對(duì)volatile的sharedObject.counter進(jìn)行寫操作之前,先對(duì)sharedObject.nonVolatile變量進(jìn)行寫操作,所以當(dāng)線程A要將volatile的sharedObject.counter寫回到主存時(shí),這兩個(gè)變量都會(huì)被寫回到主存中。
同理,線程B在讀取volatile變量到sharedObject.counter的時(shí)候,兩個(gè)變量sharedObject.counter and sharedObject.nonVolatile所以線程讀取變量sharedObject.nonVolatile就會(huì)看到他被線程A改變后的值。
開發(fā)者可以利用這個(gè)擴(kuò)展的可見性去放大線程間的變量可見性,不需要將每一個(gè)變量都聲明為volatile,只需要聲明一兩個(gè)變量為volatile就可以了。下面這個(gè)簡(jiǎn)單的例子,就來說明這個(gè)問題:
public class Exchanger {
private Object object = null;
private volatile hasNewObject = false;
public void put(Object newObject) {
while(hasNewObject) {
//wait - do not overwrite existing new object
}
object = newObject;
hasNewObject = true; //volatile write
}
public Object take(){
while(!hasNewObject){ //volatile read
//wait - don't take old object (or null)
}
Object obj = object;
hasNewObject = false; //volatile write
return obj;
}
}
線程A可能會(huì)調(diào)用put方法將objects put進(jìn)去,線程B可能會(huì)調(diào)用take方法將object拿出來。這個(gè)類可以正常工作,只要我們使用一個(gè)volatile變量即可(不使用同步語句),只要只有線程A調(diào)用put,只有線程B調(diào)用take。
然后,JVM有時(shí)候?yàn)榱颂岣咝?,可能?huì)改變指令執(zhí)行的順序,只要JVM判斷這樣做不改變指令的語義,那么就有可能改變指令的順序。那么如果JVM改變了指令的執(zhí)行順序會(huì)發(fā)生什么呢?put方法可能會(huì)像下面這樣執(zhí)行:
while(hasNewObject) {
//wait - do not overwrite existing new object
}
hasNewObject = true; //volatile write
object = newObject;
我們觀察到,現(xiàn)在對(duì)于volatile的hasNewObject 操作在object = newObject;之前執(zhí)行,這說明,object還沒有真正被賦值新對(duì)象,但是hasNewObject 已經(jīng)先變?yōu)閠rue了。對(duì)于JVM來說,這種交換是完全有可能的。因?yàn)檫@兩個(gè)write的指令彼此不是互相依賴的。
但是這樣交換順序之后可能會(huì)對(duì)object變量的可見性產(chǎn)生不好的影響。首先,線程B可能會(huì)在線程A真正給object寫入一個(gè)新值之前,就看到hasNewObject 變?yōu)閠rue。
另一方面,我們無法確保object什么時(shí)候會(huì)被真正寫入到主內(nèi)存中。
為了防止上面這種情況的發(fā)生,volatile關(guān)鍵字就提出了一種“happens before guarantee”,這可以保證volatile的變量的讀寫指令不會(huì)被重新排序。指令前面的和后面的可以隨意排序,但是volatile變量的讀寫指令的相對(duì)順序是不能改變的。
看下面這個(gè)例子就能理解了:
sharedObject.nonVolatile1 = 123;
sharedObject.nonVolatile2 = 456;
sharedObject.nonVolatile3 = 789;
sharedObject.volatile = true; //a volatile variable
int someValue1 = sharedObject.nonVolatile4;
int someValue2 = sharedObject.nonVolatile5;
int someValue3 = sharedObject.nonVolatile6;
JVM可能會(huì)改變前三個(gè)指令的順序,只要他們?cè)趘olatile的寫指令之前發(fā)生(就是說他們必須在volatile的寫指令之前發(fā)生)。
同理,JVM也可能改變后三個(gè)指令的順序,只要他們?cè)趘olatile的寫指令之后發(fā)生。
這就是對(duì)于Java的 volatile happens before guarantee.的最基本的理解
Volatile有時(shí)候也是不夠的
雖然volatile可以保證讀取操作直接從主內(nèi)存中的讀取,寫操作直接寫到內(nèi)存中,但仍然存在一些情況下,光使用volatile關(guān)鍵字是不夠的。
在之前的舉例的程序中,只有一個(gè)線程在向共享變量寫入數(shù)據(jù)的時(shí)候,聲明為volatile,另一個(gè)線程就可以一直看到最新被寫入的值。
實(shí)際上,只要新值不依賴舊值的情況下,多個(gè)線程同時(shí)向共享的volatile變量里寫入數(shù)據(jù)時(shí),仍然能在主內(nèi)存中得到正確的值。換句話說,就是這個(gè)volatile變量值更新的時(shí)候,不需要先讀取出他以前的值才能得到下一個(gè)值。
只要一個(gè)線程需要先讀取一個(gè)voaltile變量,然后必須基于他的值才能產(chǎn)生新的值,那么volatile關(guān)鍵字就不再能保證變量的可見性了。在讀取變量和寫入變量的時(shí)候,存在一個(gè)短的時(shí)間間隙,這就會(huì)造成,多個(gè)線程可能會(huì)在這個(gè)間隙讀取同一個(gè)值,產(chǎn)生一個(gè)新值,然后寫入到主內(nèi)存中,將其他線程對(duì)值的改變給覆蓋了。
所以常見的情況就是如果一個(gè)volatile變量在進(jìn)行自增或者自減操作,那么這時(shí)候使用volatile就可能出問題。
接下來我們更深入的討論這個(gè)問題,假設(shè)線程1讀取一個(gè)共享的counter變量到cpu的cache中,此時(shí)他的值是0,然后給它自增加一,但是還沒有寫到主存中,所以主存中還是1,線程2也能夠讀取同一個(gè)counter變量,而這個(gè)變量讀取的時(shí)候還是0,在他自己的cpucache中,這樣就出現(xiàn)問題了:

線程1和線程2實(shí)際上是不同步的。共享變量counter的真實(shí)值實(shí)際上應(yīng)該為2,因?yàn)楸患恿藘纱?,但是每個(gè)線程在自己的cache上存的值是1,而且在主存中這個(gè)值仍然是0,這就變得很混亂。即使線程最后將值寫回到主存中,但最后的值也是不正確的。
什么時(shí)候volatile足夠了
前文中提到,如果兩個(gè)線程都在對(duì)volatile變量進(jìn)行讀寫操作,那么僅僅使用volatile關(guān)鍵字是遠(yuǎn)遠(yuǎn)不夠的。你需要使用synchronize關(guān)鍵字,來保證讀寫操作的原子性。
但如果是只有一個(gè)線程在讀寫volatile變量,另外的多個(gè)線程僅僅是讀取這個(gè)變量的話,那么這就可以保證,其他讀線程所看到的變量值都是最新的。volatile關(guān)鍵字可以使用在32位或者64位的變量上。
volatile關(guān)鍵字對(duì)效率的影響
讀寫一個(gè)volatile變量的時(shí)候,會(huì)導(dǎo)致變量直接在主存中讀寫,顯然,直接從主存中讀寫速度要比從cache中來得慢。另一方面,操作volatile變量的時(shí)候不能改變指令的執(zhí)行順序,這一定程度上也會(huì)影響讀寫的效率。所以,只有我們需要確保變量可見性的時(shí)候,才會(huì)使用volatile關(guān)鍵字。