java volatile關鍵字解析

volatile是什麼

  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怎麼用

  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,並不針對設計模式進行討論,所以後續有時間,再補上替代上述單例的寫法。

 

 

有任何的不合適或者錯誤的地方還請留言指正。

相關文章
相關標籤/搜索