Java并發編程之volatile關鍵字解析

keithk 7年前發布 | 16K 次閱讀 Java 并發 Java開發

volatile關鍵字雖然從字面上理解起來比較簡單,但是要用好不是一件容易的事情。本文我們就從JVM內存模型開始,了解一下 volatile 的應用場景。

volatile關鍵字

JVM內存模型

在了解 volatile 之前,我們有必要對JVM的內存模型有一個基本的了解。Java的內存模型規定了所有的變量都存儲在主內存中(即物理硬件的內存),每條線程還具有自己的工作內存(工作內存可能位于處理器的高速緩存之中),線程的工作內存中保存了該線程使用到的變量的主內存副本拷貝,線程對變量的所有操作(讀取,賦值等)都必須在工作內存中進行,而不能直接讀寫主內存中的變量)。不同的線程之間無法直接訪問對方工作內存之間的變量,線程間變量值的傳遞需要通過主內存來完成。

p.s: 對于上面提到的副本拷貝,比如假設線程中訪問一個10MB的對象,并不會把這10MB的內存復制一份拷貝出來,實際上這個對象的引用,對象中某個在線程訪問到的字段是有可能存在拷貝的,但不會有虛擬機實現把整個對象拷貝一次。

在并發編程中,我們通常會遇到以下三個問題:原子性,可見性,有序性,下面我們我們來具體看一下這三個特性與 volatile 之間的聯系:

有序性

public class Testcase{
 public static int number;
 public static boolean isinited;

 public static void main(String[] args){
 new Thread(
 () -> {
 while (!isinited) {
 Thread.yield();
 }
 System.out.println(number);
 }
 ).start();
 number = 20;
 isinited = true;
 }
}

對于上面的代碼我們上面的本意是想輸出 20 ,但是如果運行的話可以發現輸出的值可能會是 0 。這是因為有時候為了提供程序的效率,JVM會做進行及時編譯,也就是可能會對指令進行重排序,將 isInited = true; 放在 number = 20; 之前執行,在單線程下面這樣做沒有任何問題,但是在多線程下則會出現重排序問題。如果我們將 number 聲名為 volatile 就可以很好的解決這個問題,這可以禁止JVM進行指令重排序,也就意味著 number = 20; 一定會在 isInited = true 前面執行。

可見性

比如對于變量 a ,當線程一要修改變量a的值,首先需要將a的值從主存復制過來,再將a的值加一,再將a的值復制回主存。在單線程下面,這樣的操作沒有任何的問題,但是在多線程下面,比如還有一個線程二,在線程一修改a的值的時候,也從主存將a的值復制過來進行加一,隨后線程一和線程二先后將a的值復制回主存,但是主存中a的值最終將只會加一而不是加二。

使用 volatile 可以解決這個問題,它可以保證在線程一修改a的值之后立即將修改值同步到主存中,這樣線程二拿到的a的值就是線程一已經修改過的a的值了。

原子性

原子性是指CPU在執行一條語句的時候,不會中途轉去執行另外的語句。比如 i = 1 就是一個原子操作,但是 ++i 就不是一個原子操作了,因為它要求首先讀取 i 的值,然后修改 i 的值,最后將值寫入主存中。

但是 volatile 卻不能保證程序的原子性,下面我們通過一個實例來驗證一下:

public class TestCase{
 public volatile int v = 0;
 public static final int threadCount = 20;

 public void increase(){
 v++;
 }

 public static void main(String[] args){
 TestCase testCase = new TestCase();
 for (int i=0; i<threadCount; i++) {
 new Thread(
 () -> {
 for (int j=0; j<1000; j++) {
 testCase.increase();
 }
 }
 ).start();
 }

 while (Thread.activeCount() > 1) {
 Thread.yield();
 }
 System.out.println(testCase.v);
 }
}

輸出結果:

上面我們的本意是想讓輸出 20000 ,但是運行程序后,結果可能會小于 20000 。因為 v++ 它本身并不是一個原子操作,它是分為多個步驟的,而且 volatile 本身也并不能保證原子性。

上面的程序使用 synchronzied 則可以很好的解決,只需要聲明 public synchronized void increase() 就行了。

或者使用lock也行:

Lock lock = new ReentrantLock();

public void increase(){
 lock.lock();
 try {
 v++;
 } finally{
 lock.unlock();
 }
}

或者將 v 聲明為 AtomicInteger v = new AtomicInteger(); 。在java 1.5的java.util.concurrent.atomic包下提供了一些原子操作類,即對基本數據類型的自增,自減,以及加法操作,減法操作進行了封裝,保證這些操作是原子性操作。

volatile的應用場景

下面我們通過單例模式來看一下 volatile 的一個具體應用:

class Singleton{
 private volatile static Singleton instance;

 public static Singleton getInstance(){
 if (instance == null) {
 synchronized (Singleton.class) {
 if (instance == null)
 instance = new Singleton();
 }
 }
 return instance;
 }

 public static void main(String[] args){
 Singleton.getInstance();
 }
}

上面 instance 必須要用 volatile 修飾,因為 new Singleton 是分為三個步驟的:

  1. 給instance指向的對象分配內存,并設置初始值為null(根據JVM類加載機制的原理,對于靜態變量這一步應該在 new Singleton 之前就已經完成了)。
  2. 執行構造函數真正初始化instance
  3. 將instance指向對象分配內存空間(分配內存空間之后instance就是非null了)

在我們的步驟2, 3之間的順序是可以顛倒的,如果線程一在執行步驟3之后并沒有執行步驟2,但是被線程二搶占了,線程二得到的 instance 是非null,但是instance卻還沒有初始化。而使用instance則可以保證程序的有序性。

References

深入理解Java虛擬機

Java并發編程實戰

 

來自:http://www.ziwenxie.site/2017/04/24/java-multithread-volatile/

 

 本文由用戶 keithk 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!