兩張圖理解volatile關鍵字

lightTrace發表於2018-09-09

最近又看了一下volatile關鍵字,以前覺得花很大功夫才理解的東西發現其實也沒自己想的那麼難,畫個模型圖然後再跑跑程式碼感覺很容易就理解了,而且記的也牢,雖然JDK1.7,JDK1.8的synchronized關鍵字已經優化的很好了,但也不能synchronized關鍵字一條道走到黑,哈哈。

  1. 記憶體模型概念

計算機執行程式時,指令都是由cpu執行的,執行指令必然會涉及到資料的讀寫,cpu執行的速度遠比從記憶體讀寫資料快,所以讀取記憶體資料成了cpu執行速度的攔路虎,所以cpu就有了一個快取記憶體。
也就是當cpu執行程式時會將程式所需的資料複製一份到cpu的快取記憶體中,完成計算後將最新資料刷到主存中,如圖

這裡寫圖片描述

2.volatile如何保證可見性?
由上圖我們思考如果兩個執行緒同時進行i=i+1操作:

這裡寫圖片描述

那麼在不做任何處理的情況下i最終會等於3嗎?
答案顯然是不一定?

假如執行緒1,執行緒2同時把i=1複製到快取記憶體,執行緒1執行完i=i+1後返回資料到主存將i重新賦值為2,然後執行緒2的快取記憶體並不知道主存的i已經變成2,它還是繼續將i+1執行結果2重新賦值給主存i,這樣經過兩次計算i的值還是2!

這就是著名的快取不一致的問題,解決這個問題一個是通過加鎖的辦法,即每次保證只有一個cpu讀到這個這個主存的i值就不會有兩個快取記憶體的衝突,但這種方式太低下,於是我們想到有沒有一種方法保證執行緒1的i值改變後立刻告訴執行緒2?

當我們想到這裡時,就離理解volatile關鍵字的精髓不遠了,當我們用volatile修飾一個共享變數時,只要一個執行緒改變這個變數,它會將最新的值更新的主存,並告訴其他執行緒改變數快取記憶體失效需重新讀取,這樣就保證了i值的可見性,也保證了i值不會被重新的不正確賦值。

3.volatile為什麼不保證原子性?
volatile有個性質就是如果變數沒有變化就不會通知其它執行緒該變數在快取記憶體無效!

i=i+1的自增操作我們知道不是原子性的:

第一步:執行緒先讀取了變數i的原始值
第二步:執行緒進行+1操作,並將值重新整理到主存

我們試想執行緒1先讀取了i的值,但此時假如它被阻塞沒有將i值變化,執行緒2的快取記憶體的變數i就不會變化,還是將i+1=2,然後將2重新整理賦值主存。此時執行緒1的快取記憶體值會變成2,但是之前執行緒1已經完成了賦值1,還會繼續進行計算i+1=2,最後將i=2重新整理到主存,結果還是錯誤的!

4.volatile怎樣保證有序性?

一般來說,處理器為了提高程式執行效率,可能會對輸入程式碼進行優化,它不保證程式中各個語句的執行先後順序同程式碼中的順序一致,但是它會保證程式最終執行結果和程式碼順序執行的結果是一致的。當然語句2依賴於語句1,那麼語句1無論什麼情況都會在語句2前執行,例如:

int i=1;//語句1
i++;//語句2

但是如果是:

//執行緒1:
context = loadContext();   //語句1
inited = true;             //語句2

//執行緒2:
while(!inited ){
  sleep() 
}
doSomethingwithconfig(context);

  上面程式碼中,由於語句1和語句2沒有資料依賴性,因此可能會被重排序。假如發生了重排序,線上程1執行過程中先執行語句2,而此是執行緒2會以為初始化工作已經完成,那麼就會跳出while迴圈,去執行doSomethingwithconfig(context)方法,而此時context並沒有被初始化,就會導致程式出錯。
  
 從上面可以看出,指令重排序不會影響單個執行緒的執行,但是會影響到執行緒併發執行的正確性。
如果我們用voliatle關鍵字對inited 修飾,那麼就會保證執行到inited = true; 的時候context = loadContext(); 已經執行完了。

volatile能禁止指令重排序(所以volatile能在一定程度上保證有序性),但是這裡只能保證volatile所修飾的變數之前的程式不會在該變數之後執行,該變數之後的程式碼不會在變數之前執行。

5.volatile使用場景??
volatile的使用場景在我看來只要保證了原子性操作的情況下都可以使用。
例如最典型的狀態變數標記:

volatile boolean flag = false;

while(!flag){
    doSomething();
}

public void setFlag() {
    flag = true;
}


volatile boolean inited = false;
//執行緒1:
context = loadContext();   
inited = true;             

//執行緒2:
while(!inited ){
sleep() 
}
doSomethingwithconfig(context);

其實通過volatile我們也瞭解了要想併發程式正確地執行,必須要保證原子性、可見性以及有序性。只要有一個沒有被保證,就有可能會導致程式執行不正確。

相關文章