Java關鍵字之volatile(可見性,有序性)

一. volatile關鍵字是什麼?

當一個變量定義爲volatile之後,它將具備兩種特性:
  ①保證此變量對所有線程的可見性
    當一條線程修改了這個變量的值,新值對於其他線程可以說是可以立即得知的。Java內存模型規定了所有的變量都存儲在主內存,每條線程還有自己的工作內存,線程的工作內存保存了該線程使用到的變量在主內存的副本拷貝,線程對變量的所有操作都必須在工作內存中進行,而不能直接讀取主內存中的變量。《深入理解Java虛擬機第二版》P363
Java內存模型
  ②禁止指令重排序優化
    普通的變量僅僅會保證在該方法的執行過程中所有依賴該賦值結果的地方都能獲得正確的結果,而不能保證變量賦值操作的順序與程序代碼中的執行順序一直。在一個線程的方法執行過程中無法感知到這點,故Java內存模型描述所謂的“線程內表示爲串行的語義”《深入理解Java虛擬機第二版》P369

二. volatile兩種特性的體現


  ①保證此變量對所有線程的可見性

/**
 * 〈volatile關鍵字特性測試〉
 *
 * @author 龍
 * @create 2018/9/7 15:21
 * @since 1.0.0
 */
public class Volatile {
    public static volatile int count=0;
    public static void main(String[] args) {
        for(int i=0;i<10;i++){
            new Thread(//開啓10個線程
                    () -> {
                        for(int j=0;j<100;j++){
                            count++;//每個線程執行加100
                        }
                        System.out.print(count+" ");
                    }
            ).start();
        }
    }
}
//運行結果:
//130 130 230 330 430 588 588 688 788 888
//100 200 300 400 500 600 700 800 900 1000
//103 103 203 303 403 503 603 703 803 903

    上面的結果理想的結果是1000,然而卻出現了其它的結果,在併發執行的過程中,哪怕一次結果不對就認爲這是不安全的。count++操作看似只有一條指令,但是在Java虛擬機層面卻已經是幾條指令的組合,如讀取count,載入count,加一,存儲count,寫入count到主內存中。假設線程A在載入count之後,線程B也載入了count,兩個線程分別加一再寫回主內存,count就寫入了兩個相同的值,本應該是加二卻只是加一。注意這並不違背可見性,畢竟在B線程讀取count的時候,A線程並沒有改變count的值,則B線程可以說依然讀取的是count的正確的結果。

那麼我們是否可以對count的加一操作進行同步已達到正確的結果那?

/**
 * 〈volatile關鍵字特性測試〉
 *
 * @author 龍
 * @create 2018/9/7 15:21
 * @since 1.0.0
 */
public class Volatile {
    //此處Volatile關鍵字可有可無,synchronized關鍵字保住了可見性
    public static Integer count=0;
    public static void main(String[] args) {
        for(int i=0;i<10;i++){
            new Thread(
                    () -> {
                        for(int j=0;j<100;j++){
                            add();//原子操作
                        }
                        System.out.print(count+" ");
                    }
            ).start();
        }

    }
    public static synchronized void add(){
        count++;//保證count加一的操作是原子的
    }
}
//運行結果:
//200 300 400 200 500 600 700 800 900 1000 
//100 200 300 437 500 600 700 800 900 1000 
//100 200 300 465 500 600 700 800 900 1000 

    可以說此時的結果已經是正確的了,原子性的加一操作就可以實現線程安全。

  ②禁止指令重排序優化

volatile boolean isOK = false;
//假設以下代碼在線程A執行 
A.init();
isOK=true;
//假設以下代碼在線程B執行
while(!isOK){
	sleep();
}
B.init();

    A線程在初始化的時候,B線程處於睡眠狀態,等待A線程完成初始化的時候才能夠進行自己的初始化。這裏的先後關係依賴於isOK這個變量。如果沒有volatile修飾isOK這個變量,那麼isOK的賦值就可能出現在A.init()之前(指令重排序,Java虛擬機的一種優化措施),此時A沒有初始化,而B的初始化就破壞了它們之前形成的那種依賴關係,可能就會出錯。

三. 什麼樣的情況使用Volatile關鍵字?


  ①確保它們自身狀態的可見性,如單例模式的雙重檢查加鎖實現
  ②標識一些重要的程序生命週期時間的發生(初始化或者關閉),如上述初始化代碼
  ③再貼出一些Volatile的使用指南:volatile用法指南




參考資料:
  《深入理解Java虛擬機第二版》
  《Java併發編程實戰》

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章