volatile是Java提供的一種輕量級的同步機制,在並發編程中,它也扮演着比較重要的角色。同synchronized相比(synchronized通常稱為重量級鎖),volatile更輕量級,相比使用synchronized所帶來的龐大開銷,倘若能恰當的合理的使用volatile,自然是美事一樁。
為了能比較清晰徹底的理解volatile,我們一步一步來分析。首先來看看如下代碼
public class TestVolatile { boolean status = false; /** * 狀態切換為true */ public void changeStatus(){ status = true; } /** * 若狀態為true,則running。 */ public void run(String t){ if(status){ System.out.println("running...." + t); } } }
上面這個例子,在多線程環境里,假設線程A執行changeStatus()方法后,線程B運行run()方法,可以保證輸出"running....."嗎?
答案是NO!
這個結論會讓人有些疑惑,可以理解。因為倘若在單線程模型里,先運行changeStatus方法,再執行run方法,自然是可以正確輸出"running...."的;但是在多線程模型中,是沒法做這種保證的。因為對於共享變量status來說,線程A的修改,對於線程B來講,是"不可見"的。也就是說,線程B此時可能無法觀測到status已被修改為true。那么什么是可見性呢?
所謂可見性,是指當一條線程修改了共享變量的值,新值對於其他線程來說是可以立即得知的。很顯然,上述的例子中是沒有辦法做到內存可見性的。
Java內存模型
為什么出現這種情況呢,我們需要先了解一下JMM(java內存模型)
java虛擬機有自己的內存模型(Java Memory Model,JMM),JMM可以屏蔽掉各種硬件和操作系統的內存訪問差異,以實現讓java程序在各種平台下都能達到一致的內存訪問效果。
JMM決定一個線程對共享變量的寫入何時對另一個線程可見,JMM定義了線程和主內存之間的抽象關系:共享變量存儲在主內存(Main Memory)中,每個線程都有一個私有的本地內存(Local Memory),本地內存保存了被該線程使用到的主內存的副本拷貝,線程對變量的所有操作都必須在工作內存中進行,而不能直接讀寫主內存中的變量。這三者之間的交互關系如下

需要注意的是,JMM是個抽象的內存模型,所以所謂的本地內存,主內存都是抽象概念,並不一定就真實的對應cpu緩存和物理內存。當然如果是出於理解的目的,這樣對應起來也無不可。
大概了解了JMM的簡單定義后,問題就很容易理解了,對於普通的共享變量來講,比如我們上文中的status,線程A將其修改為true這個動作發生在線程A的本地內存中,此時還未同步到主內存中去;而線程B緩存了status的初始值false,此時可能沒有觀測到status的值被修改了,所以就導致了上述的問題。那么這種共享變量在多線程模型中的不可見性如何解決呢?比較粗暴的方式自然就是加鎖,但是此處使用synchronized或者Lock這些方式太重量級了,有點炮打蚊子的意思。比較合理的方式其實就是volatile
volatile具備兩種特性,第一就是保證共享變量對所有線程的可見性。將一個共享變量聲明為volatile后,會有以下效應:
1.當寫一個volatile變量時,JMM會把該線程對應的本地內存中的變量強制刷新到主內存中去;
2.這個寫會操作會導致其他線程中的緩存無效。
上面的例子只需將status聲明為volatile,即可保證在線程A將其修改為true時,線程B可以立刻得知
volatile boolean status = false;
但是需要注意的是,我們一直在拿volatile和synchronized做對比,僅僅是因為這兩個關鍵字在某些內存語義上有共通之處,volatile並不能完全替代synchronized,它依然是個輕量級鎖,在很多場景下,volatile並不能勝任。看下這個例子:
public class Counter { public static volatile int num = 0; //使用CountDownLatch來等待計算線程執行完 static CountDownLatch countDownLatch = new CountDownLatch(30); public static void main(String []args) throws InterruptedException { //開啟30個線程進行累加操作 for(int i=0;i<30;i++){ new Thread(){ public void run(){ for(int j=0;j<10000;j++){ num++;//自加操作 } countDownLatch.countDown(); } }.start(); } //等待計算線程執行完 countDownLatch.await(); System.out.println(num); } }
針對這個示例,一些同學可能會覺得疑惑,如果用volatile修飾的共享變量可以保證可見性,那么結果不應該是300000么?
問題就出在num++這個操作上,因為num++不是個原子性的操作,而是個復合操作。我們可以簡單講這個操作理解為由這三步組成:
1.讀取
2.加一
3.賦值
所以,在多線程環境下,有可能線程A將num讀取到本地內存中,此時其他線程可能已經將num增大了很多,線程A依然對過期的num進行自加,重新寫到主存中,最終導致了num的結果不合預期,而是小於30000。
針對num++這類復合類的操作,可以使用java並發包中的原子操作類原子操作類是通過循環CAS的方式來保證其原子性的。
public class Counter { // public static volatile int num = 0; //使用原子操作類 public static AtomicInteger num = new AtomicInteger(0); //使用CountDownLatch來等待計算線程執行完 static CountDownLatch countDownLatch = new CountDownLatch(30); public static void main(String []args) throws InterruptedException { //開啟30個線程進行累加操作 for(int i=0;i<30;i++){ new Thread(){ public void run(){ for(int j=0;j<10000;j++){ num.incrementAndGet();//原子性的num++,通過循環CAS方式 } countDownLatch.countDown(); } }.start(); } //等待計算線程執行完 countDownLatch.await(); System.out.println(num); } }