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

漫畫:什麼是 volatile 關鍵字?

來自:程式員小灰(微信號:chengxuyuanxiaohui)





—————  第二天  —————



















————————————













Java記憶體模型簡稱JMM(Java Memory Model),是Java虛擬機所定義的一種抽象規範,用來屏蔽不同硬體和操作系統的記憶體訪問差異,讓java程式在各種平臺下都能達到一致的記憶體訪問效果。


Java記憶體模型長成什麼樣子呢?就是下圖的樣子:




這裡需要解釋幾個概念:


1.主記憶體(Main Memory)


主記憶體可以簡單理解為計算機當中的記憶體,但又不完全等同。主記憶體被所有的執行緒所共享,對於一個共享變數(比如靜態變數,或是堆記憶體中的實體)來說,主記憶體當中儲存了它的“本尊”。

2.工作記憶體(Working Memory)


工作記憶體可以簡單理解為計算機當中的CPU高速快取,但又不完全等同。每一個執行緒擁有自己的工作記憶體,對於一個共享變數來說,工作記憶體當中儲存了它的“副本”。


執行緒對共享變數的所有操作都必須在工作記憶體進行,不能直接讀寫主記憶體中的變數。不同執行緒之間也無法訪問彼此的工作記憶體,變數值的傳遞只能通過主記憶體來進行。






以上說的這些可能有點抽象,大家來看看下麵這個例子:


對於一個靜態變數 

static int s = 0;


執行緒A執行如下代碼:

s = 3;


那麼,JMM的工作流程如下圖所示:











通過一系列記憶體讀寫的操作指令(JVM記憶體模型共定義了8種記憶體操作指令,以後會細講),執行緒A把靜態變數 s=0 從主記憶體讀到工作記憶體,再把 s=3 的更新結果同步到主記憶體當中。從單執行緒的角度來看,這個過程沒有任何問題。



這時候我們引入執行緒B,執行如下代碼:


System.out.println(“s=” + s);













引入執行緒B以後,當執行緒A首先執行,更大的可能是出現下麵情況:







此時執行緒B從主記憶體得到的s值是3,理所當然輸出 s=3,這種情況不難理解。但是,有較小的幾率出現另一種情況:








因為工作記憶體所更新的變數並不會立即同步到主記憶體,所以雖然執行緒A在工作記憶體當中已經把變數s的值更新成3,但是執行緒B從主記憶體得到的變數s的值仍然是0,從而輸出 s=0。







volatile關鍵字具有許多特性,其中最重要的特性就是保證了用volatile修飾的變數對所有執行緒的可見性


這裡的可見性是什麼意思呢?當一個執行緒修改了變數的值,新的值會立刻同步到主記憶體當中。而其他執行緒讀取這個變數的時候,也會從主記憶體中拉取最新的變數值。


為什麼volatile關鍵字可以有這樣的特性?這得益於java語言的先行發生原則(happens-before)。先行發生原則在維基百科上的定義如下:


In computer science, the happened-before relation is a relation between the result of two events, such that if one event should happen before another event, the result must reflect that, even if those events are in reality executed out of order (usually to optimize program flow). 


翻譯結果如下:


在計算機科學中,先行發生原則是兩個事件的結果之間的關係,如果一個事件發生在另一個事件之前,結果必須反映,即使這些事件實際上是亂序執行的(通常是優化程式流程)。


這裡所謂的事件,實際上就是各種指令操作,比如讀操作、寫操作、初始化操作、鎖操作等等。


先行發生原則作用於很多場景下,包括同步鎖、執行緒啟動、執行緒終止、volatile。我們這裡只列舉出volatile相關的規則:


對於一個volatile變數的寫操作先行發生於後面對這個變數的讀操作。


回到上述的代碼例子,如果在靜態變數s之前加上volatile修飾符:


volatile static int s = 0;


執行緒A執行如下代碼:

s = 3;


這時候我們引入執行緒B,執行如下代碼:

System.out.println(“s=” + s);


當執行緒A先執行的時候,把s = 3寫入主記憶體的事件必定會先於讀取s的事件。所以執行緒B的輸出一定是s = 0。









這段代碼是什麼意思呢?很簡單,開啟10個執行緒,每個執行緒當中讓靜態變數count自增100次。執行之後會發現,最終count的結果值未必是1000,有可能小於1000


使用volatile修飾的變數,為什麼併發自增的時候會出現這樣的問題呢?這是因為count++這一行代碼本身並不是原子性操作,在位元組碼層面可以拆分成如下指令:


getstatic        //讀取靜態變數(count)

iconst_1        //定義常量1

iadd               //count增加1

putstatic        //把count結果同步到主記憶體


雖然每一次執行 getstatic 的時候,獲取到的都是主記憶體的最新變數值,但是進行iadd的時候,由於並不是原子性操作,其他執行緒在這過程中很可能讓count自增了很多次。這樣一來本執行緒所計算更新的是一個陳舊的count值,自然無法做到執行緒安全:











因此,什麼時候適合用volatile呢?


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


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



第一條很好理解,就是上面的代碼例子。第二條是什麼意思呢?可以看看下麵這個場景:


volatile static int start = 3;

volatile static int end = 6;


執行緒A執行如下代碼:

while (start < end){

  //do something

}


執行緒B執行如下代碼:

start+=3;

end+=3;


這種情況下,一旦在執行緒A的迴圈中執行了執行緒B,start有可能先更新成6,造成了一瞬間 start == end,從而跳出while迴圈的可能性。







幾點補充:

1. 關於volatile的介紹,本文很多內容來自《深入理解Java虛擬機》這本書。有興趣的同學可以去看看。


2.本漫畫純屬娛樂,還請大家儘量珍惜當下的工作,切勿模仿小灰的行為哦。



—————END—————



●本文編號571,以後想閱讀這篇文章直接輸入571即可

●輸入m獲取文章目錄

推薦↓↓↓

 

演算法與資料結構

更多推薦18個技術類微信公眾號

涵蓋:程式人生、演算法與資料結構、黑客技術與網絡安全、大資料技術、前端開發、Java、Python、Web開發、安卓開發、iOS開發、C/C++、.NET、Linux、資料庫、運維等。

赞(0)

分享創造快樂

© 2021 知識星球   网站地图