volatile型變數語義講解一 :對所有執行緒的可見性

長歌懷采薇發表於2020-10-13

volatile型變數語義講解一 :對所有執行緒的可見性


一、volatile變數語義一的概念

  當一個變數被定義成volatile之後,具備兩個特性:

  特性一:保證此變數對所有執行緒的可見性。這裡的“可見性”是指當一條執行緒修改了這個變數的值,新值對於其他執行緒來說是可以立即得知的。而普通變數並不能做到這一點,普通變數的值線上程傳遞時均需要通過主記憶體來完成。 比如:執行緒A修改了一個普通變數的值,然後向主記憶體進行回寫,另一條執行緒B線上程A回寫完成了之後再對主記憶體進行讀取操作,新變數值才會對執行緒B可見。

二、volatile能夠保證執行緒安全嗎

  基於volatile變數在各個執行緒中是不存在一致性問題的,從物理儲存的角度看,各個執行緒的工作記憶體中volatile變數也可以存在不一致的情況,但是由於每次使用前都要進行重新整理,執行引擎看不到不一致的情況,因此也可以人為不存在一致性問題,但是java裡面的運算操作符並非是原子操作,這導致了volatile變數的運算在併發下一樣是不安全的

案例程式碼:

/**
 * 測試Volatile的特性
 */
public class VolatileTest {
public static volatile  int race = 0;

public static void increase(){
    race++;
}
//定義執行緒的數量
private static final int THREADS_COUNT = 20;

public static void main(String[] args) {
    Thread[] threads = new Thread[THREADS_COUNT];
    for(int i = 0;i<THREADS_COUNT;i++){
        threads[i] = new Thread(new Runnable() {
            @Override
            public void run() {
               for(int j = 0;j<1000;j++){
                   increase();
               }
            }
        });
        threads[i].start();
        System.out.println("執行緒"+i+"開始執行");
    }

    while (Thread.activeCount()>2){
        System.out.println("Thread.activeCount() = "+Thread.activeCount());
        Thread.yield();//有其他執行緒等待時,將該執行緒設定為就緒狀態。
    }
    System.out.println("race:"+race);
}
}

  這段程式碼發起了20個執行緒,每個執行緒都對race變數的做了10000次的自增操作,如果是正常的併發的話,那麼race的結果用該是200000,可是執行幾次,發現結果並不是200000,而都是一個小於200000的值。這是為什麼呢? 因為++操作本身就不是原子的,要經過讀取計算和寫回,那麼,我們通過一張圖模仿一下以上程式碼:

 

 

 

 

  由於變數被volatile修飾,因此這張圖中的3,4操作是連續不間斷的,5,6,7的操作也是連續不間斷的,但是經過兩個執行緒的讀取修改寫回操作後,i的值僅僅從1變為了2,並不是我們想象的3,

可能在這裡理解上述的圖和描述有點抽象,因為有的朋友可能並不能理解資料在主存和快取中的讀取更改的傳遞規則,在這裡,補充一下變數在記憶體之間的相互操作知識點,大家可以先看以下這塊內容,再回過頭進行理解上述圖中的操作。

三:記憶體間的相互操作

·lock(鎖定):作用於主記憶體的變數,它把一個變數標識為一條執行緒獨佔的狀態。

·unlock(解鎖):作用於主記憶體的變數,它把一個處於鎖定狀態的變數釋放出來,釋放後的變數才可以被其他執行緒鎖定。

·read(讀取):作用於主記憶體的變數,它把一個變數的值從主記憶體傳輸到執行緒的工作記憶體中,以便隨後的load動作使用。

·load(載入):作用於工作記憶體的變數,它把read操作從主記憶體中得到的變數值放入工作記憶體的變數副本中。

·use(使用):作用於工作記憶體的變數,它把工作記憶體中一個變數的值傳遞給執行引擎,每當虛擬機器遇到一個需要使用變數的值的位元組碼指令時將會執行這個操作。

·assign(賦值):作用於工作記憶體的變數,它把一個從執行引擎接收的值賦給工作記憶體的變數,每當虛擬機器遇到一個給變數賦值的位元組碼指令時執行這個操作。

·store(儲存):作用於工作記憶體的變數,它把工作記憶體中一個變數的值傳送到主記憶體中,以便隨後的write操作使用。

·write(寫入):作用於主記憶體的變數,它把store操作從工作記憶體中得到的變數的值放入主記憶體的變數中。

 

 

   由volatile修飾的變數的特性保證此執行緒的可見性可知,當我們使用volatile修飾了一個變數後,一個執行緒對此變數的修改對於其他執行緒來講是立即可知的,也就是說.assign,.store,.write這三個操作是原子的,中間不會間斷,會馬上的同步主存,就像直接操作主存一樣,並通過快取一致性通知其他的快取中的副本過期。普通變數可能會在.assign,.store,.write這三個操作中插入其他的操作,導致更改後的資料不能立即同步回主存,這種情況在volatile修飾變數時是不存在的。

四:使用volatile控制併發的場景

由於volatile變數只能保證可見性,在不符合以下兩條規則的運算場景中,我們仍然要通過加鎖(使用synchronized、java.util.concurrent中的鎖或原子類)來保證原子性:

1、運算結果並不依賴變數的當前值,或者能夠確保只有單一的執行緒修改變數的值。

2、變數不需要與其他的狀態變數共同參與不變約束。

舉個例子:


class VolatileOne{
volatile boolean isShutDown;

public void shutDown(){
    isShutDown = true;
}


public void dowork(){
    while (!isShutDown){
        //業務程式碼
    }
}
}
這類場景就比較適合使用volatile控制併發,當 shutDown()方法被呼叫時,能保證所有執行緒中執行的dowork()方法都立即停下來。

使用volatile變數的第二個特性是禁止指令重排優化,我們下一篇再來分析。

 


 

補充一下,本文參考資料來源於:《深入理解Java虛擬機器:JVM高階特性與最佳實踐(第3版)》,其中講解race++操作時書上用了位元組碼進行解釋的,我這裡通過搜尋資料,通過一張圖來進行的描述,希望可以直觀的幫助大家理解。

還有,昨天寫的音樂+技術+逗比的結合寫法被朋友說寫的太亂了,很無厘頭,因此這篇中規中矩的寫了一篇,整理加自身理解,作者整理不易,覺得能幫助到您的請動動小手點個贊,如有問題請評論區提出。

相關文章