在討論volatile之前我們先來了解下cpu與記憶體之間的關係:
![Java多執行緒之初識volatile](https://i.iter01.com/images/9eedcfc07db2354a3fb70f2f15543e00907121702a77dea25a25f762b7f59c4c.png)
手殘黨、圖醜、大家心中有個大概就行了。
圖中的快取為cpu快取,實際上電腦一般設有三級快取。cpu快取為於cpu和記憶體之間的臨時儲存器,它的容量很小但交換速度卻比記憶體快得多。
快取的出現主要是為了解決CPU運算速度與記憶體讀寫速度不匹配的矛盾,因為CPU運算速度要比記憶體讀寫速度快很多,這樣會使CPU花費很長時間等待資料到來或把資料寫入記憶體。具體大家可自行百度。
volatile提供的三個特性:
-
原子性:
一個很好的例子是:32位機上的long型別讀寫操作是分為高低位讀寫兩次的,並且不原子性。所謂原子性是指一個集合操作中所有操作“同生共死”、要麼一起成功執行要麼一起不執行。
long i = 0; i = 10;
當有一條執行緒執行到
i = 10
時,首先會為低16位進行賦值,倘若此時有另一條執行緒來讀取時只會讀到只賦值的低16位的資料,從而造成bug的出現。 不過volatile並不能代替鎖,它無法保證複合操作的原子性,例如:i++
,實際上此操作是由三個步驟組成的:首先取得i的值,再對i進行+1,再將結果寫回i。 -
可見性:
就如上圖所示,每個CPU都有屬於自己的快取
int i = 0; 執行緒1執行的程式碼 i = 10; 執行緒2執行的程式碼 j = i; 複製程式碼
假設執行緒1先於執行緒2執行,並且CPU1執行執行緒1、CPU2執行執行緒2。
當執行緒1執行 i =10這句時,會先把i的初始值載入到CPU1的快取記憶體中,然後賦值為10,那麼在CPU1的快取記憶體當中i的值變為10了,卻沒有立即寫入到記憶體當中。
此時執行緒2執行 j = i,它會先去主存讀取i的值並載入到CPU2的快取當中,注意此時記憶體當中i 的值還是0,那麼就會使得j的值為0,而不是10。
這就是可見性問題,執行緒1對變數i修改了之後,執行緒2沒有立即看到執行緒1修改的值。 當一個共享變數被volatile修飾時,它會保證修改的值會立即被更新到記憶體中,當有其他執行緒需 要讀取時,它會去記憶體中讀取新值。
-
有序性:
大部分人會認為程式執行順序會和程式碼編寫順序一樣,其實不然。在JMM記憶體模型中允許編譯器和處理器對指令進行重新排序來進行優化、以便於CPU能夠並行執行指令,從而提高效率。
在單執行緒中重排序的結果不會影響程式的執行結果,但卻會影響多執行緒並行執行的正確性了。 舉個簡單的栗子:
Thread 1 Thread 2 1:r2 = A 3:r1 = b 2:b = 1 4:A = 2 複製程式碼
從順序上看 (r2 == 2) 、(r1 == 1) 應該不可能出現,但如果被重排序成下列順序是就不一定了:
Thread 1 Thread 2 b = 1 r1 = b r2 = A A = 2 複製程式碼
再舉一個稍微複雜的例子:
class order { int a = 0; boolean flag = false; public void writer() { a = 1; flag = true; } public void reader() { if (flag) { int i = a + 1; dosomething; } } } 複製程式碼
假設執行緒A先執行writer()方法,然後執行緒B執行reader()方法。當發生指令重排序後,writer()方法中的flag的寫入可能會先於a的寫入,造成執行緒B在執行reader方法時判斷正確併為i賦值。
指令重排序帶來的可見性問題
雖然指令重排序會帶來許多問題,但卻能有效提高效率,並且在序列程式碼中大家可放心:
指令重排序可以保證序列語義一致,但沒有義務保證多執行緒之間的語義也一致。
Java虛擬機器還規定了些規則指定了哪些指令不能重排序
Happen-Before 規則:
- 程式順序原則:一個執行緒內保證語義的序列性
- volatile規則:volatile變數的寫,先傳送於讀,保證volatile變數的可見性
- 鎖規則:解鎖必然先發生於隨後的加鎖前
- 傳遞性:A先於B,B先於C,那麼A必然先於C
- 執行緒的start()方法先於它的每一個動作
- 執行緒的中斷先於被中斷程式的程式碼
- 物件的建構函式的執行,結束先於finalized()方法
總的來說,volatile還涉及到JMM記憶體模型等相關知識。推薦大家去看《Java併發程式設計的藝術》和《Java高併發程式設計》,裡面講解更加透徹。