Java中volatile
關鍵字用於標記Java變量「始終存儲在主存中」.這意味着每次都是從主存中讀取volatile
修飾的變量,且每次對volatile
修飾的變量的更改都會寫回到主存中,而不是cpu緩存.html
事實上,自java5以後,volitle
關鍵字保證的不僅是始終在主存中讀取和修改volatile
修飾的變量.java
volatile
保證了線程間對共享變量的修改是可見的.緩存
在多線程應用中,對於沒有volatile
修飾的變量,每一個線程在執行過程當中都會從主存中拷貝一份變量的副本到cpu緩存中.假設計算機中擁有多個cpu,每一個線程可能在不一樣的cpu上執行,即每一個線程極可能將變量加載到不一樣的cpu緩存中.如圖所示:多線程
沒有volatile
修飾將不能保證JVM什麼時候從主存中讀取變量或什麼時候將變量寫回主存.這將致使若干問題的發生.app
假設兩個或更多的線程訪問一個包含有counter變量的共享對象,聲明以下:post
public class SharedObject{
public int counter = 0;
}
複製代碼
想象一下,當只有線程1對counter進行累加計算,但線程1和線程2在日後的時間會不定時的從主存中加載變量counter.性能
若是沒有將變量counter修飾爲volatile
將不能保證對變量counter的修改會在什麼時候寫回主存.這意味着不一樣cpu緩存中counter變量的值可能與主存中不同.如圖所示:this
問題在於線程1對counter變量的修改對於線程2不可能見.這種一個線程對共享變量的修改對於另外一個線程不可見的問題,咱們稱之爲"可見性"問題.spa
Java中 volatile
關鍵字用於解決可見性問題.若將counter修飾爲volatile
,那麼全部對於counter的修改會被當即寫回到主存中,且限制counter只能從主存中讀取.線程
public class SharedObject{
public volatile int counter = 0;
}
複製代碼
將變量修飾爲volatile
保證了變量修改對其餘線程的可見性.
上文中說起線程1對counter的修改,線程2對counter的讀取可以經過volatile
來保證線程1對counter變量的修改對於線程2可見.
然而,若是線程1和2同時累加counter變量,此時僅僅將變量修飾爲volatile
是不夠的.詳情在下文會說起.
事實上,volatile
對於可見性保障不只僅侷限於volatile
修飾的變量自己.可見性保障內容以下所示:
volatile
修飾的變量,緊接着線程2讀取一樣volatile
修飾的變量,那麼線程1對修改volatile
變量以前其餘變量的修改都會對線程2可見.volatile
修飾的變量,那麼volatile
修飾變量以後用到的其餘變量都會強制從主存中讀取以保證全部變量對於線程1可見.代碼實例:
public class MyClass {
private int years;
private int months
private volatile int days;
public void update(int years, int months, int days){
this.years = years;
this.months = months;
this.days = days;
}
}
複製代碼
update()方法用於更新三個變量,其中只有變量days是volatile
修飾的.
volatile
對可見性的充分保障意味着當線程更新days的值時,會連同days以前的yeas months更新也寫回主存中.
當讀取years months days時:
public class MyClass {
private int years;
private int months
private volatile int days;
public int totalDays() {
int total = this.days;
total += months * 30;
total += years * 365;
return total;
}
public void update(int years, int months, int days){
this.years = years;
this.months = months;
this.days = days;
}
}
複製代碼
注意totalDays()方法,一開始將days的值賦予total,緊接着連同參與計算的months和years也一塊兒從主存中讀取.所以你能夠保障上面days months和years的讀取都是最新的.
JVM和CPU可以在語義相同的狀況下對程序中的指令進行重排序以達到更好的執行效率.以下所示:
int a = 1;
int b = 2;
a++;
b++;
複製代碼
這些指令可以在語義一致的狀況下從新調整順序:
int a = 1;
a++;
int b = 2;
b++;
複製代碼
然而,當重排序中有volatile
修飾的變量時,將會帶來一些挑戰.
再來看看以前說起的實例:
public class MyClass {
private int years;
private int months
private volatile int days;
public int totalDays() {
int total = this.days;
total += months * 30;
total += years * 365;
return total;
}
public void update(int years, int months, int days){
this.years = years;
this.months = months;
this.days = days;
}
}
複製代碼
一旦update()方法更新days變量,那麼對於years和months的更新也會被寫回到主存中,若JVM進行重排序:
public void update(int years, int months, int days){
this.days = days;
this.months = months;
this.years = years;
}
複製代碼
當對days變量進行修改時,months和years的修改也會被寫回主存,但此次對days的修改是在months和years以前,所以對於months和years的最新修改不會對其餘線程可見.重排序後的語義已經發生改變.
針對指令重排序的挑戰,volatile
給出了"happens-before"保障,用於補充可見性保障.happens-before保障的內容以下所示:
happens-before保證了volatile
可見性保障的強制執行.
儘管volatile
保障了volatile
修飾的變量老是從主存中讀取和寫回主存,但仍是有些狀況即便將變量修飾爲volatile
也不能知足.
以前的狀況是線程1對於volatile
變量的修改老是對於線程2可見.
在多線程下,若是產生的新值並不依賴主存中的舊值(不須要使用舊值來推導出新值),那麼即便兩個線程同時更新主存中volatile
修飾的變量值也不會有問題.
當一個線程產生的新值須要依賴舊值時,那麼僅僅用volatile
修飾共享變量來保障可見性是不夠的.當一個線程在讀取主存中volatile
修飾的共享變量以前,此時有兩個線程同時從主存中加載相同的volatile
修飾的變量,同時進行更新且寫回主存時會產生競態條件,此時兩個線程對舊值的更新會互相覆蓋.那麼以後線程從主存中讀取的數值多是錯誤的.
這種狀況下,當兩個線程同時累加相同的counter變量時,用volatile
修飾變量已經不能知足了.以下所示:
當線程1從主存加載counter到cpu緩存中,此時counter爲0,對counter進行累加以後counter變爲1,此時線程1尚未將counter寫回主存,線程2一樣將主存中counter加載到cpu緩存中進行累加操做.此時線程2也尚未將counter寫回主存.
實際上線程1和線程2是同時進行的.而主存中counter變量的預期結果應該爲2,但如圖所示兩個線程在各自緩存中的值爲1,而在主存中的值爲0.即便兩個線程將各自緩存中的值寫回主存也是錯誤的.
兩個線程同時對一個共享變量進行讀寫操做時,使用volatile
修飾已經不能知足狀況.你須要使用synchronized
來保障變量讀寫操做的原子性.使用volatile
並不能同步線程的讀寫操做.這種狀況下只能使用synchronized
關鍵字來修飾臨界區代碼.
除了synchronized
,你還能夠選擇java.util.concurrent
包中提供的原子數據類型.如AtomicLong
和AtomicRefrerence
等.
在其餘狀況下,若是隻有一個線程對volatile
修飾的變量進行讀寫操做,其餘線程只進行讀操做,那麼volatile
是足夠保障可見性的,若沒有volatile
修飾,那就不能保障了.
volatile
關鍵字在32bit和64上的變量可用.
對於volatile
修飾變量的讀寫可以被強制在主存中進行(從主存中讀取,寫回主存).直接在主存中讀寫的性能消耗遠大於在cpu緩存中讀寫.volatile
可以在特定狀況有效防止指令重排序.因此應該謹慎使用volatile
,只有在真正須要保障變量可見性的狀況下使用.
該系列博文爲筆者複習基礎所著譯文或理解後的產物,複習原文來自Jakob Jenkov所著Java Concurrency and Multithreading Tutorial