volatile是什么
volatile在java語言中是一個關鍵字,用於修飾變量。被volatile修飾的變量后,表示這個變量在不同線程中是共享,編譯器與運行時都會注意到這個變量是共享的,因此不會對該變量進行重排序。上面這句話可能不好理解,但是存在兩個關鍵,共享和重排序。
變量的共享
先來看一個被舉爛了的例子:
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變量被分別拷貝到t1、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。循環退出。
volatile boolean isStop = false;
代碼的重排序
再來看一個被舉爛了的例子:
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之后執行。
volatile怎么用
volatile關鍵字一般用於標記變量的修飾,類似上述例子。《Java並發編程實戰》中說,volatile只保證可見性,而加鎖機制既可以確保可見性又可以確保原子性。當且僅當滿足以下條件下,才應該使用volatile變量:
1、對變量的寫入操作不依賴變量的當前值,或者確保只有單個線程變更變量的值。
2、該變量不會於其他狀態一起納入不變性條件中
3、在訪問變量的時候不需要加鎖。
逐一分析:
第一條說明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()分為三步,1、分配內存空間,2、初始化對象,3、設置instance指向被分配的地址。然而指令的重新排序,可能優化指令為1、3、2的順序。如果是單個線程訪問,不會有任何問題。但是如果兩個線程同時獲取getInstance,其中一個線程執行完1和3步驟,此時其他的線程可以獲取到instance的地址,在進行if(instance==null)時,判斷出來的結果為false,導致其他線程直接獲取到了一個未進行初始化的instance,這可能導致程序的出錯。所以用volatile修飾instance,禁止指令的重排序,保證程序能正常運行。(Bug很難出現,沒能模擬出來)。
然而,《java並發編程實戰中》中有對DCL的描述如下:"DCL的這種使用方法已經被廣泛廢棄了——促使該模式出現的驅動力(無競爭同步的執行速度很慢,以及JVM啟動很慢)已經不復存在了,因而它不是一種高效的優化措施。延遲初始化占位類模式能帶來同樣的優勢,並且更容易理解。",其實我個小碼畜的角度來看,服務端的單例更多時候做延遲初始化並沒有很大意義,延遲初始化一般用來針對高開銷的操作,並且被延遲初始化的對象都是不需要馬上使用到的。然而,服務端的單例在大部分的時候,被設計為單例的類大部分都會被系統很快訪問到。本篇文章只是討論volatile,並不針對設計模式進行討論,因此后續有時間,再補上替代上述單例的寫法。