摘要
Volatile是Java提供的一種弱同步機制,當一個變量被聲明成volatile類型後編譯器不會將該變量的操作與其他內存操作進行重排序。在某些場景下使用volatile代替鎖可以減少代碼量和使代碼更易閱讀。 Volatile特性 1.可見性:當一條線程對volatile變量進行了修改操作時,其他線程能立即知道修改的值,即當讀取一個volatile變量時總是返回最近一次寫入的值 2.原子性:對於單個voatile變量其具有原子性(能保證long double類型的變量具有原子性),但對於i ++ 這類復合操作其不具有原子性(見下面分析) 使用volatile變量的前提 1.對變量的寫入操作不依賴變量的當前值,或者能夠確保只有單一的線程修改變量的值 2.該變量不會與其他狀態變量一起納入不變性條件中 3.在訪問變量時不需要加鎖 volatile可見性 volatile的可見性正是基於happend -before(先行發生)關系實現的。 happend-before:java內存模型有八條可以保證happend-before的規則(詳見《深入理解Java虛擬機》P376),如果兩個操作之間的關系無法從這八條規則中推導出來的話,它們就沒有順序保障,虛擬機就可以對它們隨意地進行重排序. 其中就包含”volatile變量規則“:對一個volatile變量的寫操作先行發生於後面對這個變量的讀操作,此規則保證虛擬機不會對volatile讀/寫操作進行重排序。 通過一個例子來了解vloative的可見性 例1:public class VolatileTest extends Thread{ private boolean isRunning = true; public boolean isRunning(){ return isRunning; } public void setRunning(boolean isRunning){ this.isRunning= isRunning; } public void run(){ System.out.println("進入了run..............."); while (isRunning){} System.out.println("isUpdated的值被修改為為false,線程將被停止了"); } public static void main(String[] args) throws InterruptedException { VolatileTest volatileThread = new VolatileTest(); volatileThread.start(); Thread.sleep(1000); volatileThread.setRunning(false); //停止線程 } }輸出:
進入了run...............發現並沒有輸出”isUpdated的值被修改為為false,線程將被停止了”這一句,說明通過setRunning來修改isRunning的值對於該程序是不可見的,也就是說程序不知道自己的值被修改了,為什麼? 原因:Java內存模型(JMM)規定了所有的變量都存儲在主內存中,主內存中的變量為共享變量,而每條線程都有自己的工作內存,線程的工作內存保存了從主內存拷貝的變量,所有對變量的操作都在自己的工作內存中進行,完成後再刷新到主內存中,回到例1,第18行號代碼主線程(線程main)雖然對isRunning的變量進行了修改且有刷新回主內存中(《深入理解java虛擬機》中關於主內存與工作內存的交互協議提到變量在工作內存中改變後必須將該變化同步回主內存),但volatileThread線程讀的仍是自己工作內存的舊值導致出現多線程的可見性問題,解決辦法就是給isRunning變量加上volatile關鍵字。 當變量被聲明成volatile類型後,線程對該變量進行修改後會立即刷新回主內存,而其他線程讀取該變量時會先將自己工作內存中的變量置為無效,再從主內存重新讀取變量到自己的工作內存,這樣就避免發生線程可見性問題。 volatile內存語義總結如下 1.當線程對volatile變量進行寫操作時,會將修改後的值刷新回主內存 2.當線程對volatile變量進行讀操作時,會先將自己工作內存中的變量置為無效,之後再通過主內存拷貝新值到工作內存中使用。 volatile原子性 volatile並不完全具有原子性,對於復合操作其仍存在線程不安全的問題,如 例2
public class VolatileTest1{ private volatile int value; //將value變量聲明成volatile類型 public void increment(){ value ++; System.out.println(value); } public static void main(String[] args) { final VolatileTest1 volatileTest1 = new VolatileTest1(); for(int i = 0; i < 10; i ++){ new Thread(new Runnable() { public void run() { volatileTest1.increment(); } }).start(); } } }輸出:
1 6 5 4 2 3 8 7 9 10線程每次對value進行自增操作,顯然輸出結果不是我們想要的那種,這裡就出現了線程安全問題,為什麼? 像value ++這樣的操作並不具有原子性,其實際的過程如下: 當線程1在步驟2對value進行計算時,剛好其他線程也對value進行了修改,這時線程1返回的值就不是我們期望的值了,於是出現線程安全問題,所以volatile不能保證復合操作具有原子性;解決辦法就是給increment方法加鎖(lock/synchronized)或將變量聲明為原子類類型。 Synchronized與volatile區別 1.volatile只能修飾變量,而synchronized可以修改變量,方法以及代碼塊 2.volatile在多線程中不會存在阻塞問題,synchronized會存在阻塞問題 3.volatile能保證數據的可見性,但不能完全保證數據的原子性,synchronized即保證了數據的可見性也保證了原子性 4.volatile解決的是變量在多個線程之間的可見性,而sychroized解決的是多個線程之間訪問資源的同步性