歡迎光臨
每天分享高質量文章

漫畫:volatile對指令重排的影響

(點擊上方公眾號,可快速關註)


來源:伯樂專欄作者/玻璃貓,微信公眾號 – 程式員小灰

好文投稿, 請點擊 → 這裡瞭解詳情

上一期介紹了volatile關鍵字對JVM主記憶體和工作記憶體的影響,沒看過的小伙伴們可以點擊下麵鏈接:


漫畫:什麼是 volatile 關鍵字?


實在懶得去看也不要緊,我們簡單回顧一下:

volatile是一個輕量級的執行緒同步機制。它的特性之一,是保證了變數在執行緒之間的可見性




當一個執行緒修改了變數的值,新的值會立刻同步到主記憶體當中。而其他執行緒讀取這個變數的時候,也會從主記憶體中拉取最新的變數值。


但是volatile並不保證變數更新的原子性,在一些場景下,用volatile修飾的變數仍然不是執行緒安全。


下麵,我們來繼續今天的主題,講一講volatile的其他特性。









什麼是指令重排?


指令重排是指JVM在編譯Java代碼的時候,或者CPU在執行JVM位元組碼的時候,對現有的指令順序進行重新排序。


指令重排的目的是為了在不改變程式執行結果的前提下,優化程式的運行效率。需要註意的是,這裡所說的不改變執行結果,指的是不改變單執行緒下的程式執行結果。


然而,指令重排是一把雙刃劍,雖然優化了程式的執行效率,但是在某些情況下,會影響到多執行緒的執行結果。我們來看看下麵的例子:


boolean contextReady = false;


在執行緒A中執行:

context = loadContext();

contextReady = true;

 

在執行緒B中執行:

while( ! contextReady ){ 

   sleep(200);

}

doAfterContextReady (context);



以上程式看似沒有問題。執行緒B迴圈等待背景關係context的加載,一旦context加載完成,contextReady == true的時候,才執行doAfterContextReady 方法。


但是,如果執行緒A執行的代碼發生了指令重排,初始化和contextReady的賦值交換了順序:



boolean contextReady = false;


在執行緒A中執行:

contextReady = true;

context = loadContext();

 

在執行緒B中執行:

while( ! contextReady ){ 

   sleep(200);

}

doAfterContextReady (context);



這個時候,很可能context物件還沒有加載完成,變數contextReady 已經為true,執行緒B直接跳出了迴圈等待,開始執行doAfterContextReady 方法,結果自然會出現錯誤。


需要註意的是,這裡java代碼的重排只是為了簡單示意,真正的指令重排是在位元組碼指令的層面。








什麼是記憶體屏障?


記憶體屏障(Memory Barrier)是一種CPU指令,維基百科給出瞭如下定義:


A memory barrier, also known as a membar, memory fence or fence instruction, is a type of barrier instruction that causes a CPU or compiler to enforce an ordering constraint on memory operations issued before and after the barrier instruction. This typically means that operations issued prior to the barrier are guaranteed to be performed before operations issued after the barrier.


翻譯結果如下:


記憶體屏障也稱為記憶體柵欄或柵欄指令,是一種屏障指令,它使CPU或編譯器對屏障指令之前和之後發出的記憶體操作執行一個排序約束。 這通常意味著在屏障之前發佈的操作被保證在屏障之後發佈的操作之前執行。


記憶體屏障共分為四種型別:


LoadLoad屏障

抽象場景:Load1; LoadLoad; Load2

Load1 和 Load2 代表兩條讀取指令。在Load2要讀取的資料被訪問前,保證Load1要讀取的資料被讀取完畢。


StoreStore屏障:

抽象場景:Store1; StoreStore; Store2

Store1 和 Store2代表兩條寫入指令。在Store2寫入執行前,保證Store1的寫入操作對其它處理器可見


LoadStore屏障:

抽象場景:Load1; LoadStore; Store2

在Store2被寫入前,保證Load1要讀取的資料被讀取完畢。


StoreLoad屏障:

抽象場景:Store1; StoreLoad; Load2

在Load2讀取操作執行前,保證Store1的寫入對所有處理器可見。StoreLoad屏障的開銷是四種屏障中最大的。







volatile做了什麼?


在一個變數被volatile修飾後,JVM會為我們做兩件事:


1.在每個volatile寫操作前插入StoreStore屏障,在寫操作後插入StoreLoad屏障。

2.在每個volatile讀操作前插入LoadLoad屏障,在讀操作後插入LoadStore屏障。


或許這樣說有些抽象,我們看一看剛纔執行緒A代碼的例子:


boolean contextReady = false;


在執行緒A中執行:

context = loadContext();

contextReady = true;

 

我們給contextReady 增加volatile修飾符,會帶來什麼效果呢?



由於加入了StoreStore屏障,屏障上方的普通寫入陳述句 context = loadContext()  和屏障下方的volatile寫入陳述句 contextReady = true 無法交換順序,從而成功阻止了指令重排序。










volatile特性之一:


保證變數在執行緒之間的可見性。可見性的保證是基於CPU的記憶體屏障指令,被JSR-133抽象為happens-before原則。


volatile特性之二:


阻止編譯時和運行時的指令重排。編譯時JVM編譯器遵循記憶體屏障的約束,運行時依靠CPU屏障指令來阻止重排。





幾點補充:


1. 在使用volatile引入記憶體屏障的時候,普通讀、普通寫、volatile讀、volatile寫會排列組合出許多不同的場景。我們這裡只簡單列出了其中一種,有興趣的同學可以查資料進一步學習其他阻止指令重排的場景。


2.volatile除了保證可見性和阻止指令重排,還解決了long型別和double型別資料的8位元組賦值問題。這個特性相對簡單,本文就不詳細描述了。


—————END—————

漫畫演算法系列


覺得本文有幫助?請分享給更多人

關註「演算法愛好者」,修煉編程內功

淘口令複製以下紅色內容,再打開手淘即可購買

範品社,使用¥極客T恤¥搶先預覽(長按複製整段文案,打開手機淘寶即可進入活動內容)

近期,北京地區正常發貨,但派件時間有所延長。

赞(0)

分享創造快樂