背景
volatile關鍵字是併發程式設計中的一個比較重要的關鍵字。它能保證變數/物件在記憶體中的可見性,同時禁止指令重排序,避免了CPU或者編譯器最佳化帶來的可見性問題。
在併發程式設計中,volatile可以去修飾一個變數,或者是一個物件(比如單例模式中就使用了volatile去修飾單例物件)
舉例說明
volatile int a = 100;
volatile SingleInstance instance;*
什麼是可見性?什麼是可見性問題?
答: 可見性指的是一個共享變數被執行緒修改了以後,其他執行緒能立即看到變更後的變數值。因為執行緒是在各自的工作記憶體中執行資料的邏輯操作,並不會操作到主記憶體的變數值,一旦執行緒更新了變數的值,如果想要被可見,就必須立即再更新至主記憶體。所以,可見性問題就是指,A執行緒更新了變數的值,其他B執行緒操作變數的時候,沒有得到這個變數變更後的新值,也就是A執行緒修改的值不可見。B執行緒使用舊值對變數進行更新操作,從而使得資料不一致。這個就是可見性問題!
如何解決變數可見性問題?
答:java中解決可見性問題的方案有很多。比如synchronzied, volatile, Lock鎖,Atomic包下的原子類,JUC下的類。
synchronzied主要是保證同一時刻只能有一個執行緒操作某一個共享變數。避免多個執行緒同時訪問一個共享變數帶來的可見性問題。
volatile主要是會確保每個執行緒都能從主記憶體中讀取該變數的最新值,而不是從自己的快取中讀取。本篇文章主要講volatile的底層原理。
volatile的實現原理
volatile是透過記憶體屏障來禁止指令重排序,從而保證可見性的。
記憶體屏障有寫屏障和讀屏障(這些屏障實際上是一些硬體或者編譯器級別的指令), volatile變數更新以後,會立即呼叫store指令,確保之前所有的寫操作都會重新整理到主記憶體中,避免寫操作的重排序。讀屏障確保讀取volatile變數之前,會從主記憶體中讀取最新的值。大多數處理器用的都是StoreLoad屏障。
StoreLoad相當於是一個全屏障,它會把處理器給變數賦值的指令儲存到Store Buffer, 然後lock指令使到Store Buffer中的資料重新整理到快取行,同時使得其他CPU快取了變數的快取行失效。
所以說記憶體屏障底層其實還是呼叫了Lock指令。
happens before模型
這個JMM中的一些規範,主要是描述了兩個操作指令的順序關係。如果A操作和B操作存在happens-before的關係,那麼意味著A操作的執行結果對B操作可見。
以下是一些Happens-before規則
Happens-before規則
- 程式順序原則
在同一個執行緒中,如果x操作在Y操作之前,那麼x happens before y,其實也是as-if-serial語義。 - 傳遞性規則
如果存在A happens before B; B happens before C, 那麼必然會存在A happens before C 。 - volatile變數規則
指的是透過記憶體屏障來保障一個volatile修飾的變數的寫操作一定happens before於其讀操作。 - 監視器鎖規則
一個執行緒對一個鎖的釋放操作一定happens before後續執行緒對該鎖的加鎖操作。
public void monitor() {
synchronzied(this) {
if(x == 0) {
x = 10;
}
}
如程式碼所示,當A執行緒執行邏輯之前,加上鎖,執行結束後,會釋放鎖,此時A產生的結果對B一定是可見的。
- start規則
假如一個執行緒A呼叫子執行緒的start方法,那麼執行緒A在呼叫start()方法之前的所有操作都happens-before執行緒B中的所有操作。 - join規則
首先join()的作用是等待某個子執行緒的執行結果。
如果主執行緒main()執行了執行緒A的join()方法並且成功返回,那麼執行緒A中的任意操作happens before 於main執行緒的join()方法返回之後的操作。