volatile的學習總結

1.volatile是Java虛擬機提供的輕量級的同步機制
  • 保證可見性

  • 不保證原子性

  • 禁止指令重排

2. Java內存模型(JMM)

JMM(Java內存模型Java Memory Model,簡稱JMM)本身是一種抽象的概念,並不真實存在,它描述的是一組規則或規範,通過規範定義了程序中的各個變量(包括實例字段、靜態字段和構成數組對象的元素)的訪問方式。

JMM的同步規定:

  1. 線程解鎖前,必須把共享變量的值刷新回主內存

  2. 線程加鎖前,必須讀取主內存的最新值到自己的工作內存

  3. 加鎖解鎖是同一把鎖

由於JVM運行程序的實體是線程,而每個線程創建時JVM都會爲其創建一個工作內存(有些地方稱爲棧空間),工作內存時每個線程的私有數據區域,而Java內存模型中規定所有變量都存儲在主內存,主內存是共享內存區域,所有線程都可以訪問,但線程對變量的操作(讀取賦值等)必須在工作內存中進行,首先要將變量從主內存拷貝到自己的工作內存空間,然後對變量進行操作,操作完成後再將變量寫回到主內存,不能直接操作主內存中的變量,各個線程的工作內存中存儲着主內存中的變量副本拷貝,因此不同的線程間無法訪問對方的工作內存,線程間的通信(傳值)必須通過主內存來完成,其簡要的訪問過程如下圖:

在這裏插入圖片描述

JMM的三大特性

JMM是線程安全性獲得的保證。因爲JMM具有如下特點:

  1. 可見性:從主內存拷貝變量後,如果某一個線程在自己的工作內存中對變量進行了修改,然後寫回了主內存,其它線程能第一時間看到,這就叫作可見性。

  2. 原子性:不可分割,完整性,也即某個線程正在做某個具體業務時,中間不可以被加塞或者被分割

  3. 有序性:禁止指令重排,按照規定的順序去執行

綜上所述,volatile滿足JMM三大特性中的兩個,即可見性和有序性,volatile並不滿足原子性,所以說volatile是輕量級的同步機制。

3. 代碼驗證Volatile的可見性

代碼示例:

	/**
	 * Created by salmonzhang on 2020/7/4.
	 * 可見性代碼實例
	 */
	public class VolatileDemo {
	    public static void main(String[] args) {
	        MyData myData = new MyData();
	        new Thread(() -> {
	            System.out.println(Thread.currentThread().getName() + "\t come in ...");
	            //暫停一會兒線程
	            try{ TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); }
	            myData.addTo10();
	            System.out.println(Thread.currentThread().getName() + "\t update number value:" + myData.number);
	        },"Thread01").start();
	
	        while (myData.number == 0) {
	            //main線程一直在這裏等待,直到number的值不再等於零
	        }
	        System.out.println(Thread.currentThread().getName()+"\t mission is over , number updated ...");
	    }
	}
	
	class MyData{
	//    int number = 0; // 這裏沒有加volatile
	    volatile int number = 0; // 這裏加了volatile
	    public void addTo10() {
	        this.number = 10;
	    }
	}

沒有加volatile的運行結果:

在這裏插入圖片描述

加了volatile的運行結果:

在這裏插入圖片描述

總結:如果不加volatile關鍵字,則主線程會進入死循環,加了volatile時主線程運行正常,可以正常退出,說明加了volatile關鍵字後,當有一個線程修改了變量的值,其它線程會在第一時間知道,當前值作廢,重新從主內存中獲取值。這種修改變量的值,讓其它線程第一時間知道,就叫作可見性。

4. 代碼驗證Volatile不保證原子性

代碼示例:

	/**
	 * Created by salmonzhang on 2020/7/4.
	 * 驗證volatile不保證原子性
	 * 原子性是什麼意思:
	 * 不可分割,完整性,也即某個線程正在做某個具體業務時,中間不可以被加塞或者被分割。
	 * 需要整體完整,要麼同時成功,要麼同時失敗。保證數據的原子一致性
	 */
	public class VolatileDemo2 {
	    public static void main(String[] args) {
	        MyData2 myData2 = new MyData2();
	        for (int i = 1; i <= 20; i++){
	            new Thread(() -> {
	                for (int j = 0; j < 1000; j++) {
	                    myData2.addPlusPuls();
	                }
	            },String.valueOf(i)).start();
	        }
	
	        //需要等待上面20個線程全部執行完成後,再用main線程取得最終的結果值看看是多少?
	        while (Thread.activeCount() > 2) { //後臺默認有兩個線程:GC線程和main線程
	            Thread.yield();
	        }
	        System.out.println(Thread.currentThread().getName() + "finally number value = " + myData2.number);
	    }
	}
	
	class MyData2{
	    volatile int number = 0; // 這裏加了volatile
	    public void addPlusPuls() {
	        number++;
	    }
	}

運行結果:

在這裏插入圖片描述

從代碼的運行結果會發現:會出現number最終的結果有可能出現不是20000的時候,這就證明了volatile不能保證原子性。

5. volatile不能保證原子性的原因和解決方案
  1. 爲什麼volatile不能保證原子性?

    由於多線程進程調度的關係,在某一時間段出現了丟失寫值的情況。因爲線程切換太快,會出現後面的線程會把前面的線程的值剛好覆蓋。

    例如:Thread1和Thread2同時從主內存中讀取number的值1到自己的工作內存,並同時進行了+1的動作,當Thread1將2寫會主內存的時候,由於線程的調度原因,Thread2並沒有第一時間知道Thread1已經將number的值改爲了2,而是直接將Thread1改的number值進行覆蓋,這樣就會導致數據丟失。

  2. 解決方案:

    2.1. 直接在addPlusPuls前面加上synchronized

    class MyData2{
        volatile int number = 0; // 這裏加了volatile
        public synchronized void addPlusPuls() {
            number++;
        } 	
    }
    

    但是爲了保證一個number++的原子性直接用synchronized,感覺有點重,類似於“殺雞用牛刀”

    2.2 用atomic

    class MyData2{
        AtomicInteger number = new AtomicInteger();
        public void addPlusPuls() {
            number.getAndIncrement();
        } 	
    }
    
7. 有序性
  1. 計算機在執行程序時,爲了提高性能,編譯器的處理器通常會對指令做重排,一般有三種重排:

    • 編譯器的重排

    • 指令並行的重排

    • 內存系統的重排

在這裏插入圖片描述

  1. 單線程環境裏確保程序最終執行的結果和代碼執行的結果一致

  2. 處理器在進行重排序時,必須考慮指令之間的數據依懶性

  3. 多線程環境中線程交替執行,由於編譯器優化重排的存在,兩個線程中使用的變量能否保證用的變量能否一致性是無法確定的,結果也是無法預測的

重排案例一:

public void mySort(){
    int x=11;//語句1
    int y=12;//語句2
    x=x+5;//語句3
    y=x*x;//語句4
}

計算機執行的順序可能是:

1234

2134

1324

問題:
請問語句4可以重排後變成第一條碼?
存在數據的依賴性,所以沒辦法排到第一個

重排案例二:
在這裏插入圖片描述

指令重排代碼示例:

public class ReSortSeqDemo {
    int a = 0;
    boolean flag = false;
    
    public void method01() {
        a = 1;           // 這裏的a和flag沒有禁止指令重排,所以在多線程環境中就有可能出現問題
        flag = true;
    }

    public void method02() {
        if (flag) {
            a = a + 3;
            System.out.println("a = " + a);
        }
    }
}

這裏的a和flag沒有禁止指令重排,所以在多線程環境中就有可能出現問題,例如指令重排後,method01中的flag=true先被Thread1執行了,此時Thread2又搶佔到了線程資源去執行method02()時,此時的運行結果就是有問題的。運行結果就是a = 3,而不是正常情況下的a = 4

7. 單例模式下可能存在線程不安全

代碼示例:

public class SingletonDemo {
    private static SingletonDemo instance = null;
    private SingletonDemo(){
        System.out.println(Thread.currentThread().getName() + "\t 我是SingletonDemo的構造方法");
    };

    //synchronized 解決單例的多線程問題,會顯得比較重,整個方法都被鎖住了,不建議這麼寫
    public static SingletonDemo getInstance(){
        if (instance == null) {
            instance = new SingletonDemo();
        }
        return instance;
    }

    public static void main(String[] args) {
        //併發多線程後,會出現構造函數多次執行的情況
        for (int i = 1; i <= 10; i++){
            new Thread(() -> {
                SingletonDemo.getInstance();
            },String.valueOf(i)).start();
        }
    }
}

運行結果:

在這裏插入圖片描述

8. 單例模式下的volatile分析

1.代碼示例:

public class SingletonDemo {
    private static volatile SingletonDemo instance = null; //加上volatile,禁止編譯器指令重排
    private SingletonDemo(){
        System.out.println(Thread.currentThread().getName() + "\t 我是SingletonDemo的構造方法");
    };

    /**
     * DCL (double check Lock 雙端檢索機制)
     */
    public static SingletonDemo getInstance(){
        if (instance == null) {
            synchronized (SingletonDemo.class) {
                if (instance == null) {
                    instance = new SingletonDemo();
                }
            }
        }
        return instance;
    }

    public static void main(String[] args) {
        //併發多線程後,會出現構造函數多次執行的情況
        for (int i = 1; i <= 10; i++){
            new Thread(() -> {
                SingletonDemo.getInstance();
            },String.valueOf(i)).start();
        }
    }
}

總結:

  • 如果沒有加 volatile 就不一定是線程安全的,原因是指令重排序的存在,加入 volatile 可以禁止指令重排。

  • 原因是在於某一個線程執行到第一次檢測,讀取到的 instance 不爲 null 時,instance 的引用對象可能還沒有完成初始化。

  • instance = new Singleton() 可以分爲以下三步完成

     memory = allocate();  // 1.分配對象空間 	
     instance(memory);     // 2.初始化對象
     instance = memory;    // 3.設置instance指向剛分配的內存地址,此時instance != null
    
  • 步驟 2 和步驟 3 不存在依賴關係,而且無論重排前還是重排後程序的執行結果在單線程中並沒有改變,因此這種優化是允許的。

  • 發生重排

     memory = allocate();  // 1.分配對象空間 	
     instance = memory;    //3.設置instance指向剛分配的內存地址,此時instance != null,但對象還沒有初始化完成 
     instance(memory);     // 2.初始化對象
    
  • 所以不加 volatile 返回的實例不爲空,但可能是未初始化的實例

非常感謝您的耐心閱讀,希望我的文章對您有幫助。歡迎點評、轉發或分享給您的朋友或技術羣。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章