Java 並發編程:volatile的使用及其原理,javavolatile

來源:互聯網
上載者:User

Java 並發編程:volatile的使用及其原理,javavolatile

Java並發編程系列【未完】:

  • Java 並發編程:核心理論 

  • Java並發編程:Synchronized及其實現原理

  • Java並發編程:Synchronized底層最佳化(輕量級鎖、偏向鎖)

  • Java 並發編程:線程間的協作(wait/notify/sleep/yield/join)

  • Java 並發編程:volatile的使用及其原理

 一、volatile的作用

  在<a href="http://www.cnblogs.com/paddix/p/5374810.html">《Java並發編程:核心理論》</a>一文中,我們已經提到過可見度、有序性及原子性問題,通常情況下我們可以通過Synchronized關鍵字來解決這些個問題,不過如果對Synchronized原理有瞭解的話,應該知道Synchronized是一個比較重量級的操作,對系統的效能有比較大的影響,所以,如果有其他解決方案,我們通常都避免使用Synchronized來解決問題。而volatile關鍵字就是Java中提供的另一種解決可見度和有序性問題的方案。對於原子性,需要強調一點,也是大家容易誤解的一點:對volatile變數的單次讀/寫操作可以保證原子性的,如long和double類型變數,但是並不能保證i++這種操作的原子性,因為本質上i++是讀、寫兩次操作。

二、volatile的使用

  關於volatile的使用,我們可以通過幾個例子來說明其使用方式和情境。

1、防止重排序

  我們從一個最經典的例子來分析重排序問題。大家應該都很熟悉單例模式的實現,而在並發環境下的單例實現方式,我們通常可以採用雙重檢查加鎖(DCL)的方式來實現。其源碼如下:

 1 package com.paddx.test.concurrent; 2  3 public class Singleton { 4     public static volatile Singleton singleton; 5  6     /** 7      * 建構函式私人,禁止外部執行個體化 8      */ 9     private Singleton() {};10 11     public static Singleton getInstance() {12         if (singleton == null) {13             synchronized (singleton) {14                 if (singleton == null) {15                     singleton = new Singleton();16                 }17             }18         }19         return singleton;20     }21 }

  現在我們分析一下為什麼要在變數singleton之間加上volatile關鍵字。要理解這個問題,先要瞭解對象的構造過程,執行個體化一個對象其實可以分為三個步驟:

  (1)分配記憶體空間。

  (2)初始化對象。

  (3)將記憶體空間的地址賦值給對應的引用。

但是由於作業系統可以對指令進行重排序,所以上面的過程也可能會變成如下過程:

  (1)分配記憶體空間。

  (2)將記憶體空間的地址賦值給對應的引用。

  (3)初始化對象

  如果是這個流程,多線程環境下就可能將一個未初始化的對象引用暴露出來,從而導致不可預料的結果。因此,為了防止這個過程的重排序,我們需要將變數設定為volatile類型的變數。

2、實現可見度

  可見度問題主要指一個線程修改了共用變數值,而另一個線程卻看不到。引起可見度問題的主要原因是每個線程擁有自己的一個快取區——線程工作記憶體。volatile關鍵字能有效解決這個問題,我們看下下面的例子,就可以知道其作用:

 1 package com.paddx.test.concurrent; 2  3 public class VolatileTest { 4     int a = 1; 5     int b = 2; 6  7     public void change(){ 8         a = 3; 9         b = a;10     }11 12     public void print(){13         System.out.println("b="+b+";a="+a);14     }15 16     public static void main(String[] args) {17         while (true){18             final VolatileTest test = new VolatileTest();19             new Thread(new Runnable() {20                 @Override21                 public void run() {22                     try {23                         Thread.sleep(10);24                     } catch (InterruptedException e) {25                         e.printStackTrace();26                     }27                     test.change();28                 }29             }).start();30 31             new Thread(new Runnable() {32                 @Override33                 public void run() {34                     try {35                         Thread.sleep(10);36                     } catch (InterruptedException e) {37                         e.printStackTrace();38                     }39                     test.print();40                 }41             }).start();42 43         }44     }45 }

  直觀上說,這段代碼的結果只可能有兩種:b=3;a=3 或 b=2;a=1。不過運行上面的代碼(可能時間上要長一點),你會發現除了上兩種結果之外,還出現了第三種結果:

......
b=2;a=1
b=2;a=1
b=3;a=3
b=3;a=3
b=3;a=1
b=3;a=3
b=2;a=1
b=3;a=3
b=3;a=3
......

  為什麼會出現b=3;a=1這種結果呢?正常情況下,如果先執行change方法,再執行print方法,輸出結果應該為b=3;a=3。相反,如果先執行的print方法,再執行change方法,結果應該是 b=2;a=1。那b=3;a=1的結果是怎麼出來的?原因就是第一個線程將值a=3修改後,但是對第二個線程是不可見的,所以才出現這一結果。如果將a和b都改成volatile類型的變數再執行,則再也不會出現b=3;a=1的結果了。

3、保證原子性

   關於原子性的問題,上面已經解釋過。volatile只能保證對單次讀/寫的原子性。這個問題可以看下JLS中的描述:

17.7 Non-Atomic Treatment of double and long

For the purposes of the Java programming language memory model, a single write to a non-volatile long or double value is treated as two separate writes: one to each 32-bit half. This can result in a situation where a thread sees the first 32 bits of a 64-bit value from one write, and the second 32 bits from another write.

Writes and reads of volatile long and double values are always atomic.

Writes to and reads of references are always atomic, regardless of whether they are implemented as 32-bit or 64-bit values.

Some implementations may find it convenient to divide a single write action on a 64-bit long or double value into two write actions on adjacent 32-bit values. For efficiency's sake, this behavior is implementation-specific; an implementation of the Java Virtual Machine is free to perform writes to long and double values atomically or in two parts.

Implementations of the Java Virtual Machine are encouraged to avoid splitting 64-bit values where possible. Programmers are encouraged to declare shared 64-bit values as volatile or synchronize their programs correctly to avoid possible complications.

  這段話的內容跟我前面的描述內容大致類似。因為long和double兩種資料類型的操作可分為高32位和低32位兩部分,因此普通的long或double類型讀/寫可能不是原子的。因此,鼓勵大家將共用的long和double變數設定為volatile類型,這樣能保證任何情況下對long和double的單次讀/寫操作都具有原子性。

  關於volatile變數對原子性保證,有一個問題容易被誤解。現在我們就通過下列程式來示範一下這個問題:

 1 package com.paddx.test.concurrent; 2  3 public class VolatileTest01 { 4     volatile int i; 5  6     public void addI(){ 7         i++; 8     } 9 10     public static void main(String[] args) throws InterruptedException {11         final  VolatileTest01 test01 = new VolatileTest01();12         for (int n = 0; n < 1000; n++) {13             new Thread(new Runnable() {14                 @Override15                 public void run() {16                     try {17                         Thread.sleep(10);18                     } catch (InterruptedException e) {19                         e.printStackTrace();20                     }21                     test01.addI();22                 }23             }).start();24         }25 26         Thread.sleep(10000);//等待10秒,保證上面程式執行完成27 28         System.out.println(test01.i);29     }30 }

大家可能會誤認為對變數i加上關鍵字volatile後,這段程式就是安全執行緒的。大家可以嘗試運行上面的程式。下面是我本地啟動並執行結果:

註:上面幾段代碼中多處執行了Thread.sleep()方法,目的是為了增加並發問題的產生幾率,無其他作用。

三、volatile的原理

  通過上面的例子,我們基本應該知道了volatile是什麼以及怎麼使用。現在我們再來看看volatile的底層是怎麼實現的。

  1、可見度實現:

  在前文中已經提及過,線程本身並不直接與主記憶體進行資料的互動,而是通過線程的工作記憶體來完成相應的操作。這也是導致線程間資料不可見的本質原因。因此要實現volatile變數的可見度,直接從這方面入手即可。對volatile變數的寫操作與普通變數的主要區別有兩點:

  (1)修改volatile變數時會強制將修改後的值重新整理的主記憶體中。

  (2)修改volatile變數後會導致其他線程工作記憶體中對應的變數值失效。因此,再讀取該變數值的時候就需要重新從讀取主記憶體中的值。

  通過這兩個操作,就可以解決volatile變數的可見度問題。

  2、有序性實現:

   在解釋這個問題前,我們先來瞭解一下Java中的happen-before規則,JSR 133中對Happen-before的定義如下:

Two actions can be ordered by a happens-before relationship.If one action happens before another, then the first is visible to and ordered before the second.

  通俗一點說就是如果a happen-before b,則a所做的任何操作對b是可見的。(這一點大家務必記住,因為happen-before這個詞容易被誤解為是時間的前後)。我們再來看看JSR 133中定義了哪些happen-before規則:

• Each action in a thread happens before every subsequent action in that thread.
• An unlock on a monitor happens before every subsequent lock on that monitor.
• A write to a volatile field happens before every subsequent read of that volatile.
• A call to start() on a thread happens before any actions in the started thread.
• All actions in a thread happen before any other thread successfully returns from a join() on that thread.
• If an action a happens before an action b, and b happens before an action c, then a happens before c.

  翻譯過來為:

  • 同一個線程中的,前面的操作 happen-before 後續的操作。(即單線程內按代碼順序執行。但是,在不影響在單線程環境執行結果的前提下,編譯器和處理器可以進行重排序,這是合法的。換句話說,這一是規則無法保證編譯重排和指令重排)。
  • 監視器上的解鎖操作 happen-before 其後續的加鎖操作。(Synchronized 規則)
  • 對volatile變數的寫操作 happen-before 後續的讀操作。(volatile 規則)
  • 線程的start() 方法 happen-before 該線程所有的後續操作。(線程啟動規則)
  • 線程所有的操作 happen-before 其他線程在該線程上調用 join 返回成功後的操作。
  • 如果 a happen-before b,b happen-before c,則a happen-before c(傳遞性)。

  這裡我們主要看下第三條:volatile變數的保證有序性的規則。<a href="http://www.cnblogs.com/paddix/p/5374810.html">《Java並發編程:核心理論》</a>一文中提到過重排序分為編譯器重排序和處理器重排序。為了實現volatile記憶體語義,JMM會對volatile變數限制這兩種類型的重排序。下面是JMM針對volatile變數所規定的重定序表:

Can Reorder 2nd operation
1st operation Normal Load
Normal Store
Volatile Load Volatile Store
Normal Load
Normal Store


No
Volatile Load No No No
Volatile store
No No

  3、記憶體屏障

  為了實現volatile可見度和happen-befor的語義。JVM底層是通過一個叫做“記憶體屏障”的東西來完成。記憶體屏障,也叫做記憶體柵欄,是一組處理器指令,用於實現對記憶體操作的順序限制。下面是完成上述規則所要求的記憶體屏障:

Required barriers 2nd operation
1st operation Normal Load Normal Store Volatile Load Volatile Store
Normal Load


LoadStore
Normal Store


StoreStore
Volatile Load LoadLoad LoadStore LoadLoad LoadStore
Volatile Store

StoreLoad StoreStore

(1)LoadLoad 屏障
執行順序:Load1—>Loadload—>Load2
確保Load2及後續Load指令載入資料之前能訪問到Load1載入的資料。

(2)StoreStore 屏障
執行順序:Store1—>StoreStore—>Store2
確保Store2以及後續Store指令執行前,Store1操作的資料對其它處理器可見。

(3)LoadStore 屏障
執行順序: Load1—>LoadStore—>Store2
確保Store2和後續Store指令執行前,可以訪問到Load1載入的資料。

(4)StoreLoad 屏障
執行順序: Store1—> StoreLoad—>Load2
確保Load2和後續的Load指令讀取之前,Store1的資料對其他處理器是可見的。

最後我可以通過一個執行個體來說明一下JVM中是如何插入記憶體屏障的:

 1 package com.paddx.test.concurrent; 2  3 public class MemoryBarrier { 4     int a, b; 5     volatile int v, u; 6  7     void f() { 8         int i, j; 9 10         i = a;11         j = b;12         i = v;13         //LoadLoad14         j = u;15         //LoadStore16         a = i;17         b = j;18         //StoreStore19         v = i;20         //StoreStore21         u = j;22         //StoreLoad23         i = u;24         //LoadLoad25         //LoadStore26         j = b;27         a = i;28     }29 }

四、總結

  總體上來說volatile的理解還是比較困難的,如果不是特別理解,也不用急,完全理解需要一個過程,在後續的文章中也還會多次看到volatile的使用情境。這裡暫且對volatile的基礎知識和原來有一個基本的瞭解。總體來說,volatile是並發編程中的一種最佳化,在某些情境下可以代替Synchronized。但是,volatile的不能完全取代Synchronized的位置,只有在一些特殊的情境下,才能適用volatile。總的來說,必須同時滿足下面兩個條件才能保證在並發環境的安全執行緒:

  (1)對變數的寫操作不依賴於當前值。

  (2)該變數沒有包含在具有其他變數的不變式中。

  

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.