volatile在java語言中是一個關鍵字,用於修飾變量。被volatile修飾的變量後,表示這個變量在不一樣線程中是共享,編譯器與運行時都會注意到這個變量是共享的,所以不會對該變量進行重排序。上面這句話可能很差理解,可是存在兩個關鍵,共享和重排序。java
先來看一個被舉爛了的例子:編程
1 public class VolatileTest { 2 3 boolean isStop = false; 4 5 public void test() { 6 Thread t1 = new Thread() { 7 @Override 8 public void run() { 9 isStop = true; 10 } 11 }; 12 Thread t2 = new Thread() { 13 @Override 14 public void run() { 15 while (!isStop) { 16 } 17 } 18 }; 19 t2.start(); 20 t1.start(); 21 } 22 23 public static void main(String args[]) throws InterruptedException { 24 new VolatileTest().test(); 25 } 26 }
(注:線程2中,while內容裏若是寫個System.out.prientln(""),致使循環退出,目前沒明白什麼緣由。)設計模式
上面的代碼是一種典型用法,檢查某個標記(isStop)的狀態判斷是否退出循環。可是上面的代碼有可能會結束,也可能永遠不會結束。由於每個線程都擁有本身的工做內存,當一個線程讀取變量的時候,會把變量在本身內存中拷貝一份。以後訪問該變量的時候都經過訪問線程的工做內存,若是修改該變量,則將工做內存中的變量修改,而後再更新到主存上。這種機制讓程序能夠更快的運行,然而也會遇到像上述例子這樣的狀況。多線程
存在一種狀況,isStop變量被分別拷貝到t一、t2兩個線程中,此時isStop爲false。t2開始循環,t1修改本地isStop變量稱爲true,並將isStop=true回寫到主存,可是isStop已經在t2線程中拷貝過一份,t2循環時候讀取的是t2 工做內存中的isStop變量,而這個isStop始終是false,程序死循環。咱們稱t2對t1更新isStop變量的行爲是不可見的。併發
若是isStop變量經過volatile進行修飾,t2修改isStop變量後,會當即將變量回寫到主存中,並將t1裏的isStop失效。t1發現本身變量失效後,會從新去主存中訪問isStop變量,而此時的isStop變量已經變成true。循環退出。jvm
volatile boolean isStop = false;
再來看一個被舉爛了的例子:ide
1 //線程1: 2 context = loadContext(); //語句1 3 inited = true; //語句2 4 5 //線程2: 6 while(!inited ){ 7 sleep() 8 } 9 doSomethingwithconfig(context);
(注:感受很難模擬,我沒能模擬出來,也沒找到他人的模擬結果)測試
如上代碼示例,按照正常的想法,context初始化後,再把inited賦值爲true。可是有可能有語句2先執行,再執行語句1的狀況。致使線程2中doSomeThingWithConfig報錯。由於jvm對代碼進行編譯的時候會進行指令優化,調整互不關聯的兩行代碼執行順序,在單線程的時候,指令優化會保證優化後的結果不會出錯。可是在多線程的時候,可能發生像上述例子裏的問題。若是上述的inited用volatile修飾,就不會有問題。優化
《深刻理解Java虛擬機》中有一句話:「觀察加入volatile關鍵字和沒有加入volatile關鍵字時所生成的彙編代碼發現,加入volatile關鍵字時,會多出一個lock前綴指令」,lock前綴指令生成一個內存屏障。保證重排序後的指令不會越過內存屏障,即volatile以前的代碼只會在volatile以前執行,volaiter以後的代碼只會在volatile以後執行。this
volatile關鍵字通常用於標記變量的修飾,相似上述例子。《Java併發編程實戰》中說,volatile只保證可見性,而加鎖機制既能夠確保可見性又能夠確保原子性。當且僅當知足如下條件下,才應該使用volatile變量:
一、對變量的寫入操做不依賴變量的當前值,或者確保只有單個線程變動變量的值。
二、該變量不會於其餘狀態一塊兒歸入不變性條件中
三、在訪問變量的時候不須要加鎖。
逐一分析:
第一條說明volatile不能做爲多線程中的計數器,計數器的count++操做,分爲三步,第一步先讀取count的數值,第二步count+1,第三步將count+1的結果寫入count。volatile不能保證操做的原子性。上述的三步操做中,若是有其餘線程對count進行操做,就可能致使數據出錯。
第二條:
1 public class VolatileTest { 2 3 4 private volatile int lower = 0; 5 private volatile int upper = 5; 6 7 public int getLower() { 8 return lower; 9 } 10 11 public int getUpper() { 12 return upper; 13 } 14 15 public void setLower(int lower) { 16 if (lower > upper) { 17 return; 18 } 19 this.lower = lower; 20 } 21 22 public void setUpper(int upper) { 23 if (upper < lower) { 24 return; 25 } 26 this.upper = upper; 27 } 28 }
上述程序中,lower初始爲0,upper初始爲5,而且upper和lower都用volatile修飾。咱們指望無論怎麼修改upper或者lower,都能保證upper>lower恆成立。然而若是同時有兩個線程,t1調用setLower,t2調用setUpper,兩線程同時執行的時候。有可能會產生upper<lower這種不指望的結果。
測試代碼:
1 public void test() { 2 Thread t1 = new Thread() { 3 @Override 4 public void run() { 5 try { 6 Thread.sleep(10); 7 } catch (InterruptedException e) { 8 e.printStackTrace(); 9 } 10 setLower(4); 11 } 12 }; 13 Thread t2 = new Thread() { 14 @Override 15 public void run() { 16 try { 17 Thread.sleep(10); 18 } catch (InterruptedException e) { 19 e.printStackTrace(); 20 } 21 setUpper(3); 22 } 23 }; 24 25 t1.start(); 26 t2.start(); 27 28 while (t1.isAlive() || t2.isAlive()) { 29 30 } 31 System.out.println("(low:" + getLower() + ",upper:" + getUpper() + ")"); 32 33 } 34 35 public static void main(String args[]) throws InterruptedException { 36 for (int i = 0; i < 100; i++) { 37 VolatileTest volaitil = new VolatileTest(); 38 volaitil.test(); 39 } 40 }
輸出結果:
此時程序一直正常運行,可是出現的結果倒是咱們不想要的。
第三條:當訪問一個變量須要加鎖時,通常認爲這個變量須要保證原子性和可見性,而volatile關鍵字只能保證變量的可見性,沒法保證原子性。
最後貼個volatile的常見例子,在單例模式雙重檢查中的使用:
1 public class Singleton { 2 3 private static volatile Singleton instance=null; 4 5 private Singleton(){ 6 } 7 8 public static Singleton getInstance(){ 9 if(instance==null){ 10 synchronized(Singleton.class){ 11 if(instance==null){ 12 instance=new Singleton(); 13 } 14 } 15 } 16 return instance; 17 } 18 19 }
new Singleton()分爲三步,一、分配內存空間,二、初始化對象,三、設置instance指向被分配的地址。然而指令的從新排序,可能優化指令爲一、三、2的順序。若是是單個線程訪問,不會有任何問題。可是若是兩個線程同時獲取getInstance,其中一個線程執行完1和3步驟,此時其餘的線程能夠獲取到instance的地址,在進行if(instance==null)時,判斷出來的結果爲false,致使其餘線程直接獲取到了一個未進行初始化的instance,這可能致使程序的出錯。因此用volatile修飾instance,禁止指令的重排序,保證程序能正常運行。(Bug很難出現,沒能模擬出來)。
然而,《java併發編程實戰中》中有對DCL的描述以下:"DCL的這種使用方法已經被普遍廢棄了——促使該模式出現的驅動力(無競爭同步的執行速度很慢,以及JVM啓動很慢)已經不復存在了,於是它不是一種高效的優化措施。延遲初始化佔位類模式能帶來一樣的優點,而且更容易理解。",其實我個小碼畜的角度來看,服務端的單例更多時候作延遲初始化並無很大意義,延遲初始化通常用來針對高開銷的操做,而且被延遲初始化的對象都是不須要立刻使用到的。然而,服務端的單例在大部分的時候,被設計爲單例的類大部分都會被系統很快訪問到。本篇文章只是討論volatile,並不針對設計模式進行討論,所以後續有時間,再補上替代上述單例的寫法。