volatile的應用和原理初探

volatile 也就是JVM提供的輕量級的同步機制
有如下三個特性:

1、保證可見性

可見性的意思是當一個線程 修改一個共享變量時,另外一個線程能讀到這個修改的值
volatile是如何來保證可見性的呢?讓我們在X86處理器下通過工具獲取JIT編譯器生成的 彙編指令來查看對volatile進行寫操作時,CPU會做什麼事情。
Java代碼如下。
instance = new Singleton(); // instance是volatile變量
轉變成彙編代碼,如下。
0x01a3de1d: movb $0×0,0×1104800(%esi);0x01a3de24: lock addl $0×0,(%esp);
有volatile變量修飾的共享變量進行寫操作的時候會多出第二行彙編代碼
Lock前綴的指令在多核處理器下會引發了兩件事
1)將當前處理器緩存行的數據寫回到系統內存。
2)這個寫回內存的操作會使在其他CPU裏緩存了該內存地址的數據無效
引用於《java併發編程的藝術》

舉例說明一下
首先回顧一下JMM的八種操作
lock (鎖定):作用於主內存的變量,把一個變量標識爲線程獨佔狀態
unlock (解鎖):作用於主內存的變量,它把一個處於鎖定狀態的變量釋放出來,釋放後的變量纔可以被其他線程鎖定
read (讀取):作用於主內存變量,它把一個變量的值從主內存傳輸到線程的工作內存中,以便隨後的load動作使用
load (載入):作用於工作內存的變量,它把read操作從主存中變量放入工作內存中
use (使用):作用於工作內存中的變量,它把工作內存中的變量傳輸給執行引擎,每當虛擬機遇到一個需要使用到變量的值,就會使用到這個指令
assign (賦值):作用於工作內存中的變量,它把一個從執行引擎中接受到的值放入工作內存的變量副本中
store (存儲):作用於主內存中的變量,它把一個從工作內存中一個變量的值傳送到主內存中,以便後續的write使用
write  (寫入):作用於主內存中的變量,它把store操作從工作內存中得到的變量的值放入主內存的變量中
在這裏插入圖片描述
這時就會出現一個問題,一旦在線程B中將主存讀取出來的內容進行修改,則線程A和主存的內容將不統一,即線程A不能及時可見
在這裏插入圖片描述
代碼測試

public class JmmDemo {
    private  static int num=0;
    public static void main(String[] args) {
        new Thread(()->{
            while(num==0){

            }
        }).start();
   try{
       TimeUnit.SECONDS.sleep(1);
   }catch (InterruptedException e){
       e.printStackTrace();
   }
   num=1;
        System.out.println(num);
    }
}

在沒有volatile關鍵字的情況下,在num被賦值爲1之後,Thread會一直循環不會結束,在這個線程中,num無法檢測到被賦值了,所以會一直在while循環裏,明年也不會結束
在這裏插入圖片描述
如果加了volatile,程序會立刻結束,這也證明了volatile的可見性

而可見性的根本原因是volatile的特性—緩存一致性
線程中的處理器會一直在總線上嗅探其內部緩存中的內存地址在其他處理器的操作情況,一旦嗅探到某處處理器打算修改其內存地址中的值,而該內存地址剛好也在自己的內部緩存中,那麼處理器就會強制讓自己對該緩存地址的無效。所以當該處理器要訪問該數據的時候,由於發現自己緩存的數據無效了,就會去主存中訪問。
所以在這裏當thread嗅探到main的內存地址需要修改的時候,會強制將自己的num=0作廢,重新讀number=1,自然程序就結束了,也就保證了可見性
在這裏插入圖片描述

2、不保證原子性

原子性,也就是不可分割性,也就是不可被打斷或者插入
在這裏插入圖片描述

public class Demo2 {
    private static int number=0;
    public static void add(){
        
        number++;
    }

    public static void main(String[] args) {
        for (int i = 0; i <20 ; i++) {
            new Thread(()->{
                for (int j = 0; j < 1000; j++) {
                    add();
                }
            }).start();
        }
        while(Thread.activeCount()>2){
            Thread.yield();
        }
        System.out.println(Thread.currentThread().getName()+" "+number);
    }
}

這段代碼乍一看應該是輸出了20000個線程,但結果發現
即使加上了volatile,依然不能避免這個問題,說明volatile是不保證原子性的
在這裏插入圖片描述
很難輸出到20000,因爲這段代碼不能保證原子性,number++不是一個原子操作
通過字節碼文件可以看到
在這裏插入圖片描述
add中的number++經歷了 獲取值,+1,放回值,幾個操作,所以並不是原子性的,可能會有多個線程同時進來,拿了值改回去,比如此時線程1進去拿出來了number=0,在讀取到執行引擎中進行+1操作,再整個三步過程還沒完成的時候,線程2進來了,又把主內存中的number=0拿走了,在他的執行引擎中進行操作,並完成了number+1的工作,返回到主內存中,此時線程1和線程2都進行了+1操作,但主內存的Number卻等於1,這就出現了線程不安全的問題。這時就要用到原子更新基本類型類AtomicInteger

public class Demo2 {
    private static AtomicInteger number=new AtomicInteger();
    public static void add(){

        number.getAndIncrement();
    }

    public static void main(String[] args) {
        for (int i = 0; i <20 ; i++) {
            new Thread(()->{
                for (int j = 0; j < 1000; j++) {
                    add();
                }
            }).start();
        }
        while(Thread.activeCount()>2){
            Thread.yield();
        }
        System.out.println(Thread.currentThread().getName()+" "+number);
    }
}

此時就能輸出兩萬了

3、禁止指令重排

在jvm中,程序不是按照我們寫的方式執行的,jvm會對其進行調優,假如執行 int a = 1這句代碼需要100ms的時間,但執行int b = 2這句代碼需要1ms的時間,並且先執行哪句代碼並不會對a,b最終的值造成影響。那當然是先執行int b = 2這句代碼了。
所以,虛擬機在進行代碼編譯優化的時候,對於那些改變順序之後不會對最終變量的值造成影響的代碼,是有可能將他們進行重排序的。且很有可能會影響結果

重排序必須遵守as-if-serial原則

double pi = 3.14; //A 
double r = 1.0; // B 
double area = pi * r * r; // C

依賴關係如圖
在這裏插入圖片描述
這種情況下,c需要a也需要b的時候,重排序只能有兩種可能
在這裏插入圖片描述
volatile的使用就可以在禁止重排序的情況下,提高程序的高可用性。

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