volatile詳解

  • 2019 年 10 月 5 日
  • 筆記

版權聲明:本文為部落客原創文章,遵循 CC 4.0 BY-SA 版權協議,轉載請附上原文出處鏈接和本聲明。

本文鏈接:https://blog.csdn.net/qq_37933685/article/details/81042706

個人部落格:https://suveng.github.io/blog/​​​​​​​

volatile關鍵字

作用

1)保證了不同執行緒對這個變數進行操作時的可見性,即一個執行緒修改了某個變數的值,這新值對其他執行緒來說是立即可見的。 2)禁止進行指令重排序

程式碼實例

如下程式碼,執行緒1去修改flag為true,當修改成功時,主執行緒就會列印字元,並跳出循環

當flag欄位沒有volatile關鍵字修飾時,程式卻陷入死循環。

而引入volatile關鍵字後,正常退出。

原因就是每個執行緒工作時,會把主存的變數拷貝一份到執行緒中,執行緒1改變了flag的值,並寫入到主存中,而主執行緒的flag在執行緒1寫入主存中前就拷貝到主執行緒了。並且經過虛擬機優化後的程式碼,指令重新排序了。while循環是CPU沒有重新從主存中獲取修改後的flag的值,所以才陷入死循環。

package JUC.volatile實例;    /**   * @author Veng Su [email protected]   * @date 2018/7/14 10:41   */  public class TestVolatile {        public static void main(String[] args) {          ThreadDemo td = new ThreadDemo();          new Thread(td).start();            while(true){              if(td.isFlag()){                  System.out.println("------------------");                  break;              }          }        }    }    class ThreadDemo implements Runnable {        private volatile boolean flag = false;        @Override      public void run() {            try {              Thread.sleep(200);          } catch (InterruptedException e) {          }            flag = true;            System.out.println("flag=" + isFlag());        }        public boolean isFlag() {          return flag;      }        public void setFlag(boolean flag) {          this.flag = flag;      }    }

什麼是指令重排?

引用自https://www.cnblogs.com/dolphin0520/p/3920373.html

概念

指令重排序是JVM為了優化指令,提高程式運行效率,在不影響單執行緒程式執行結果的前提下,儘可能地提高並行度。編譯器、處理器也遵循這樣一個目標。注意是單執行緒。多執行緒的情況下指令重排序就會給程式設計師帶來問題。

例子

如果一個操作不是原子的,就會給JVM留下重排的機會。下面看幾個例子:

例子1:A執行緒指令重排導致B執行緒出錯 對於在同一個執行緒內,這樣的改變是不會對邏輯產生影響的,但是在多執行緒的情況下指令重排序會帶來問題。看下面這個情景:

在執行緒A中:

context = loadContext();  inited = true;

在執行緒B中:

while(!inited ){ //根據執行緒A中對inited變數的修改決定是否使用context變數     sleep(100);  }  doSomethingwithconfig(context);

假設執行緒A中發生了指令重排序:

inited = true;  context = loadContext();

那麼B中很可能就會拿到一個尚未初始化或尚未初始化完成的context,從而引發程式錯誤。

例子2:指令重排導致單例模式失效 我們都知道一個經典的懶載入方式的雙重判斷單例模式:

public class Singleton {    private static Singleton instance = null;    private Singleton() { }    public static Singleton getInstance() {       if(instance == null) {          synchronzied(Singleton.class) {             if(instance == null) {                 <strong>instance = new Singleton();  //非原子操作             }          }       }       return instance;     }  }

看似簡單的一段賦值語句:instance= new Singleton(),但是很不幸它並不是一個原子操作,其實際上可以抽象為下面幾條JVM指令:

memory =allocate();    //1:分配對象的記憶體空間  ctorInstance(memory);  //2:初始化對象  instance =memory;     //3:設置instance指向剛分配的記憶體地址

上面操作2依賴於操作1,但是操作3並不依賴於操作2,所以JVM是可以針對它們進行指令的優化重排序的,經過重排序後如下:

memory =allocate();    //1:分配對象的記憶體空間  instance =memory;     //3:instance指向剛分配的記憶體地址,此時對象還未初始化  ctorInstance(memory);  //2:初始化對象

可以看到指令重排之後,instance指向分配好的記憶體放在了前面,而這段記憶體的初始化被排在了後面。

在執行緒A執行這段賦值語句,在初始化分配對象之前就已經將其賦值給instance引用,恰好另一個執行緒進入方法判斷instance引用不為null,然後就將其返回使用,導致出錯。

禁止指令重排序的意思

1)當程式執行到volatile變數的讀操作或者寫操作時,在其前面的操作的更改肯定全部已經進行,且結果已經對後面的操作可見;在其後面的操作肯定還沒有進行; 2)在進行指令優化時,不能將在對volatile變數訪問的語句放在其後面執行,也不能把volatile變數後面的語句放到其前面執行。 可能上面說的比較繞,舉個簡單的例子:

//x、y為非volatile變數  //flag為volatile變數    x = 2;        //語句1  y = 0;        //語句2  flag = true;  //語句3  x = 4;         //語句4  y = -1;       //語句5

由於flag變數為volatile變數,那麼在進行指令重排序的過程的時候,不會將語句3放到語句1、語句2前面,也不會講語句3放到語句4、語句5後面。但是要注意語句1和語句2的順序、語句4和語句5的順序是不作任何保證的。

並且volatile關鍵字能保證,執行到語句3時,語句1和語句2必定是執行完畢了的,且語句1和語句2的執行結果對語句3、語句4、語句5是可見的。

volatile實現禁止指令重排的原理

volatile關鍵字通過提供「記憶體屏障」的方式來防止指令被重排序,為了實現volatile的記憶體語義,編譯器在生成位元組碼時,會在指令序列中插入記憶體屏障來禁止特定類型的處理器重排序。

大多數的處理器都支援記憶體屏障的指令。

對於編譯器來說,發現一個最優布置來最小化插入屏障的總數幾乎不可能,為此,Java記憶體模型採取保守策略。下面是基於保守策略的JMM記憶體屏障插入策略:

在每個volatile寫操作的前面插入一個StoreStore屏障。

在每個volatile寫操作的後面插入一個StoreLoad屏障。

在每個volatile讀操作的前面插入一個LoadLoad屏障。

在每個volatile讀操作的後面插入一個LoadStore屏障。

引用自http://ifeve.com/memory-barriers-or-fences/

記憶體屏障是什麼?

概念

記憶體屏障或記憶體柵欄,也就是讓一個CPU處理單元中的記憶體狀態對其它處理單元可見的一項技術。

硬體層的記憶體屏障分為兩種:Load Barrier 和 Store Barrier即讀屏障和寫屏障。

作用:

  1. 阻止屏障兩側的指令重排序;
  2. 強制把寫緩衝區/高速快取中的臟數據等寫回主記憶體,讓快取中相應的數據失效。

對於Load Barrier來說,在指令前插入Load Barrier,可以讓高速快取中的數據失效,強制從新從主記憶體載入數據; 對於Store Barrier來說,在指令後插入Store Barrier,能讓寫入快取中的最新數據更新寫入主記憶體,讓其他執行緒可見

引用自https://www.jianshu.com/p/2ab5e3d7e510

java記憶體屏障

java的記憶體屏障通常所謂的四種即LoadLoad,StoreStore,LoadStore,StoreLoad實際上也是上述兩種的組合,完成一系列的屏障和數據同步功能。 LoadLoad屏障:對於這樣的語句Load1; LoadLoad; Load2,在Load2及後續讀取操作要讀取的數據被訪問前,保證Load1要讀取的數據被讀取完畢。 StoreStore屏障:對於這樣的語句Store1; StoreStore; Store2,在Store2及後續寫入操作執行前,保證Store1的寫入操作對其它處理器可見。 LoadStore屏障:對於這樣的語句Load1; LoadStore; Store2,在Store2及後續寫入操作被刷出前,保證Load1要讀取的數據被讀取完畢。 StoreLoad屏障:對於這樣的語句Store1; StoreLoad; Load2,在Load2及後續所有讀取操作執行前,保證Store1的寫入對所有處理器可見。它的開銷是四種屏障中最大的。在大多數處理器的實現中,這個屏障是個萬能屏障,兼具其它三種記憶體屏障的功能

參考文獻:

https://www.cnblogs.com/dolphin0520/p/3920373.html http://ifeve.com/memory-barriers-or-fences/ http://www.importnew.com/23535.html https://www.jianshu.com/p/2ab5e3d7e510