java volatile關鍵字解析


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

 

 

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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM