Volatile實現原理


 讀寫volatile變量就像是訪問一個同步塊一樣,是原子的且是可見的,總是能訪問到最新的值。

原子性

 讀寫volatile變量是原子操作,但讀寫變量不就是一條指令的事嗎(mov、ldr),難道這還可分?沒錯絕大多數變量讀寫都是原子的,除了在32位JVM下對long、double的讀寫,就不是原子的。這是因為在32位下,總線寬度就只有32bit,對64位數據的讀寫需要分兩次進行,依次讀寫高低32位。但是讀寫volatile變量由於使用了LOCK前綴指令,鎖住了內存,所以即使是64位的數據也是原子的。

讀寫volatile變量是原子的,包括64位的long和double

實現原子性

 實現64位的原子性,需要在讀寫volatile變量時,使用Lock前綴指令,其作用有:

  1. 鎖住該內存地址,直到讀完/寫完,保證64位變量讀寫原子性。少量處理器是使用鎖總線實現的,相比鎖內存,其開銷更大,鎖總線期間,所有處理器都不能操作主存外存。
  2. 將寫緩存刷新到主存,保證可見性
  3. 禁止該指令與前面和后面的讀寫指令重排序,保證happens-before關系

可見性

happens-before中定義了:寫volatile變量,happens-before后面任意一個讀這個volatile變量的操作

 這意味着volatile變量在多線程間具有可見性從源碼到Runtime發生的重排序指出重排序破壞了可見性。為實現volatile的可見性,讀寫volatile時則需要禁止重排序,那么需要禁止編譯器重排序處理器重排序

happens-before關系

happens-before規則

  1. 程序順序規則:在一個線程中,前面的操作happens-before后面的操作
  2. volatile寫-讀規則:寫volatile變量,happens-before后面任意一個讀這個volatile變量的操作
  3. 傳遞性規則:A happens-before B,B happens-before C,則A happens-before C

 從這段代碼看看happens-before關系,線程A先執行store(),線程B后執行load()

int value = 0;
volatile boolean finish = false;

void store(){
    value = 1;      //A
    read(value);    //B
    finish = true;  //C
    value = 2;      //D
    read(value);    //E
}

void load(){
    value = 3;      //F
    read(value);    //G
    while(!finish); //H
    assert value == 1;  //I
    value = 4;      //J
}

 ①~⑧是程序順序規則,⑨是volatile寫-讀規則,淺色的是傳遞性規則后面詳細解釋這些關系。

happens-before關系

從happends-before規則分析可見性

①~⑧是根據程序順序規則得出的,程序順序規則前提是僅考慮本線程的可見性,那么就不需要考慮多個處理器引發的緩存不一致問題,不需要考慮內存系統重排序,所以不需要用到內存屏障。這樣就很簡單了,只要保證其在單線程內運行結果不變即可,只要保證編譯器、處理器不重排數據依賴的指令

是根據volatile域寫-讀規則得出的得出:C happens-before H。也就是線程A寫volatile happens-before 線程B讀volatile

 再根據傳遞性規則得出:ABC happens-before H 。也就是線程A寫volatile及其之前的操作 happens-before 線程B讀volatile

 再根據傳遞性規則得出:ABC happens-before HIJ 。最終得出線程A寫volatile及其之前的操作 happens-before 線程B讀volatile及其后續操作

 這樣來看,寫volatile時,需要馬上將本地內存刷新到主存中去。讀volatile時,需要將本地內存中共享變量設為無效狀態,重新從主存中讀。

編譯器層面實現可見性

編譯器處理volatile變量重排序規則表
可以看到:

  • 寫volatile變量時,無論前一個操作是什么,都不能重排序(①~④happens-before)
  • 讀volatile變量時,無論后一個操作是什么,都不能重排序(⑤~⑧happens-before)
  • 當先寫volatile變量,后讀volatile變量時,不能重排序(⑨happens-before)

處理器層面實現可見性

根據前面的出來的可見性:線程A寫volatile及其之前的操作 happens-before 線程B讀volatile及其后續操作

 可以看到這個可見性是在多線程間的,所以要避免內存系統重排序,需要使用JMM提供的內存屏障

![內存屏障](https://note.youdao.com/yws/api/personal/file/26850C1996884379959E4075A9F6D1D5?method=download&shareKey=c7457c11dcae9bf05cebe8c1749d23be “內存屏障”)

 先給可見性拆分,方便從最簡單的開始實現:

  1. 線程A寫volatile happens-before 線程B讀volatile
  2. 線程A寫volatile及其之前的操作 happens-before 線程B讀volatile
  3. 線程A寫volatile及其之前的操作 happens-before 線程B讀volatile及其后續操作

 實現可見性:

  1. 對第一級可見性,可以在寫volatile之后加StoreLoad Barrier,也可以在讀volatile之前加StoreLoad Barrier。選擇哪種?在實際開發中,常用的模式是一個寫線程,多個讀線程,典型的有生產者消費者模式,所以在寫volatile后加StoreLoad Barrier,會大大減少執行屏障的次數,比后者的性能要好。
  2. 對第二級可見性,在寫volatile之前加上StoreStore Barrier,可以保證寫volatile之前,其之前的所有操作結果已經可見。不用LoadStore Barrier的原因是:讀操作並不會改變操作結果。
  3. 對第三級可見性,實際上是保證讀volatile后續操作會不會和讀volatile重排序。那么就在讀volatile后面加LoadLoad Barrier,這樣保證讀volatile在其后續讀操作之前執行,這樣的話 線程A 對 讀volatile的后續讀操作也可見。同理為了使線程A 對 讀volatile后續寫操作可見,在讀volatile后加上LoadStore Barrier。

 綜上所述:

  1. 寫volatile之前,StoreStore Barrier
  2. 寫volatile之后,StoreLoad Barrier
  3. 讀volatile之后,LoadLoad Barrier 和 LoadStore Barrier

 在剛才的例子上添加內存屏障,實現happens-before關系。

int value = 0;
volatile boolean finish = false;

void store(){
    value = 1;      //A
    read(value);    //B
    storeStoreBarrier();
    finish = true;  //C
    storeLoadBarrier();
    value = 2;      //D
    read(value);    //E
}

void load(){
    value = 3;      //F
    read(value);    //G
    while(!finish); //H
    loadLoadBarrier();
    loadStoreBarrier();
    assert value == 1;  //I
    value = 4;      //J
}


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM