Volatile關鍵字剖析

weixin_33807284發表於2018-10-15

參考部落格:https://www.cnblogs.com/dolphin0520/p/3920373.html

基本都是按照這個部落格上面說的去理解,然後寫出來.建議去看一下這個部落格!

volatile這個關鍵字目前只在建立單例(懶漢式單例的時候)的時候用到,因此理解還不夠深刻,只能按照自己理解去寫一下,加深一下記憶!

Volatile關鍵字

1.記憶體模型相關概念

​ 我們知道,計算機在執行程式時,每條指令都是在CPU中執行的,而執行指令過程中,勢必涉及到資料的讀取和寫入操作.由於程式執行過程中的資料存放在主存(物理內容)中,這就存在CPU執行效率較高,而記憶體讀取/寫入資料速度較慢,這會大大的降低指令執行的速度,因此在CPU裡面就有了高度快取.

快取記憶體的出現會解決CPU讀取/寫入慢的問題,這在單執行緒中是適用的,但在多執行緒中就會出執行緒不安全問題;假如多個執行緒共享同一個資料,那麼這個資料數值的改變可能就不會通知其他執行緒,也就是出現了快取不一致問題,通常有2種解決方法:

​ 1.在匯流排上加LOCK#鎖的方式(因為會存在阻塞情況,所以效率低);

​ 2.通過快取一致性協議(快取一致性協議中較為出名的是Intel MESI協議,MESI協議保證了每個快取中使用的共享變數的副本是一致的.它核心的思想是:當CPU寫資料時,如果發現操作的變數時共享變數,即在其它CPU中也存下該變數的副本,會發出訊號通知其它CPU將該變數的快取設定為無效狀態,因此當其它CPU需要讀取這個變數時,發現自己快取中快取該變數的快取行是無效的,那麼它會從記憶體中重新讀取);

CUP <---------> Cache <---------> 快取一致性協議(或總鎖機制) <---------> 主存(實體記憶體)

2.併發程式設計中的三個概念

2.1.原子性:

可以事務的原子性進行比較.也就是說:一個操作或者多個操作,要麼全部執行並執行過程未被中斷,要麼全不執行;

超級典型的例子就是銀行轉賬例子了!想想,,你給你父母裝了1000,但他們賬戶錢數如果沒有增加,會怎樣,很崩潰是嗎?

2.2.可見性

可見性是指當多個執行緒訪問同一個變數時,一個執行緒修改了這個變數的值,其它執行緒能夠立即得到修改的值;

2.3.有序性:

即程式執行的順序按照程式碼的先後順序執行;

這裡需要說明一下指令重排序,一般來說,處理器為了提高程式執行效率,可能會對輸入程式碼進行優化,它不保證各個語句的執行先後順序同程式碼中的順序一致,但它會保證程式最終執行結果和程式碼順序執行的結果一致.指令重排序不會影響單個執行緒的執行,但是會影響到執行緒併發執行的正確性;

3.Java記憶體模型

Java記憶體模型(Java Memory Model,JVM),java記憶體模型中,也存在一致性問題和指令重排序問題;

3.1原子性

在Java中,對基本資料型別的變數的讀取和賦值操作時原子性操作,即這些操作時不可被中斷的,要麼執行,要麼不執行;

Java記憶體模型只保證了基本讀取和賦值是原子性操作,如果要實現更大範圍操作的原子性,需要通過synchronizedLock來實現.它們可以保證任何時候都只有一個執行緒執行該程式碼塊,那麼自然就不存在原子性問題了,從而保證了原子性;

3.2可見性

對於可見性,Java提供了volatile關鍵字來保證可見性;

當一個共享變數被volatile修飾時,它會保證修改的值會立即被更新到主存,當有其他執行緒需要讀取時,它會去記憶體中讀取新值.

另外,也可以通過synchronizedLock來保證可見性;

3.3有序性

​ 在Java記憶體模型中,允許編譯器和處理器對指令進行重排序,但是重排序過程不會影響大單執行緒程式的執行,卻回影響到多執行緒併發執行的正確性;

​ Java提供了volatile關鍵字來保證一定的"有序性",另外,也可以通過synchronizedLock來保證有序性;

另外,Java記憶體模型具備一些先天的“有序性”,即不需要通過任何手段就能夠得到保證的有序性,這個通常也稱為 happens-before 原則。如果兩個操作的執行次序無法從happens-before原則推匯出來,那麼它們就不能保證它們的有序性,虛擬機器可以隨意地對它們進行重排序。

  下面就來具體介紹下happens-before原則(先行發生原則):

程式次序規則:一個執行緒內,按照程式碼順序,書寫在前面的操作先行發生於書寫在後面的操作
鎖定規則:一個unLock操作先行發生於後面對同一個鎖額lock操作
volatile變數規則:對一個變數的寫操作先行發生於後面對這個變數的讀操作
傳遞規則:如果操作A先行發生於操作B,而操作B又先行發生於操作C,則可以得出操作A先行發生於操作C
執行緒啟動規則:Thread物件的start()方法先行發生於此執行緒的每個一個動作
執行緒中斷規則:對執行緒interrupt()方法的呼叫先行發生於被中斷執行緒的程式碼檢測到中斷事件的發生
執行緒終結規則:執行緒中所有的操作都先行發生於執行緒的終止檢測,我們可以通過Thread.join()方法結束、Thread.isAlive()的返回值手段檢測到執行緒已經終止執行
物件終結規則:一個物件的初始化完成先行發生於他的finalize()方法的開始
  這8條原則摘自《深入理解Java虛擬機器》。

  這8條規則中,前4條規則是比較重要的,後4條規則都是顯而易見的。

  下面我們來解釋一下前4條規則:
  
  對於程式次序規則來說,我的理解就是一段程式程式碼的執行在單個執行緒中看起來是有序的。注意,雖然這條規則中提到“書寫在前面的操作先行發生於書寫在後面的操作”,這個應該是程式看起來執行的順序是按照程式碼順序執行的,因為虛擬機器可能會對程式程式碼進行指令重排序。雖然進行重排序,但是最終執行的結果是與程式順序執行的結果一致的,它只會對不存在資料依賴性的指令進行重排序。因此,在單個執行緒中,程式執行看起來是有序執行的,這一點要注意理解。事實上,這個規則是用來保證程式在單執行緒中執行結果的正確性,但無法保證程式在多執行緒中執行的正確性。

  第二條規則也比較容易理解,也就是說無論在單執行緒中還是多執行緒中,同一個鎖如果出於被鎖定的狀態,那麼必須先對鎖進行了釋放操作,後面才能繼續進行lock操作。

  第三條規則是一條比較重要的規則,也是後文將要重點講述的內容。直觀地解釋就是,如果一個執行緒先去寫一個變數,然後一個執行緒去進行讀取,那麼寫入操作肯定會先行發生於讀操作。

  第四條規則實際上就是體現happens-before原則具備傳遞性。

4.剖析volatile關鍵字

1.volatile的兩層含義

1)保證了不同執行緒對這個變數進行操作時的可見性,即一個執行緒修改額某個變數的值,這新值對其它執行緒來說是立即可見的;

2)禁止進行指令重排序.

2.volatile保證原子性嗎?

無法保證

3.volatile可以保證有序性嗎?

因為vloatile關鍵字能禁止指令重排序,所以能在一定程式上保證有序性.

volatile關鍵字禁止指令重排序有兩層意思:

1)當程式執行到volatile變數的讀操作或者寫操作時,在其前面的操作的更改肯定全部已經進行,且結果已經對後面的操作可見;在其後面的操作肯定還沒有進行;

2)在進行指令優化時,不能將在對volatile變數訪問的語句放在其後面執行,也不能把volatile變數後面的語句放到其前面執行。

4.volatile的原理和實現機制

加入volatile關鍵字和沒有加入volatile關鍵字時所生成的彙編程式碼發現,加入volatile關鍵字時,會多出一個lock字首指令!!!lock字首指令實際上相當於一個記憶體屏障,記憶體屏障會提供3個功能:

  1. 確保指定重排序時不會把其後面的指令排到記憶體屏障之前的位置,也不會把前面的指令排到記憶體屏障的後面;也就是說在執行到記憶體屏障這句指令時,它前面的操作已全部完成;
  2. 它會強制對快取的修改操作立即寫入主存;
  3. 如果是寫操作,它會導致其他CPU中對應的快取行無效;

5.volatile使用場景

synchronized關鍵字是防止多個執行緒同時執行一段程式碼,那麼就會很影響程式執行效率,而volatile關鍵字在某些情況下效能要優於synchronized,但是要注意volatile關鍵字是無法替代synchronized關鍵字的,因為volatile關鍵字無法保證操作的原子性。通常來說,使用volatile必須具備以下2個條件:

1)對變數的寫操作不依賴於當前值

2)該變數沒有包含在具有其他變數的不變式中

實際上,這些條件表明,可以被寫入 volatile 變數的這些有效值獨立於任何程式的狀態,包括變數的當前狀態。

事實上,我的理解就是上面的2個條件需要保證操作是原子性操作,才能保證使用volatile關鍵字的程式在併發時能夠正確執行。

相關文章