Java記憶體模型——volatile關鍵字

逍遙遊007發表於2019-06-23

  最近工作中又用到了volatile關鍵字,一直以來就是單純的使用,也沒有仔細看過相關內容,這次藉機會詳細的整理了下有關volatile的資料,記錄在案以備查閱。

  首先我們來看一個小例子:

 1 public class VolatileDemo1 {
 2     private boolean flag = true;
 3 
 4     public static void main(String[] args) throws InterruptedException {
 5         VolatileDemo1 demo = new VolatileDemo1();
 6         Thread thread = new Thread(() -> {
 7             long start = System.currentTimeMillis();
 8             while (demo.flag) {
 9             }
10             long end = System.currentTimeMillis();
11             System.out.println("終止了while迴圈!flag的值為:" + demo.flag);
12             System.out.println("耗時:" + ( end -start ));
13         });
14         thread.start();
15         TimeUnit.SECONDS.sleep(2);
16         demo.flag = false;
17     }
18 }

  這段程式碼是volatile關鍵字的典型應用場景之一,兩個執行緒(主執行緒與thread 執行緒)通過共享一個變數進行資訊互動,在上一段程式碼中,由於沒有為flag變數加上volatile關鍵字,可以預見,執行緒thread中的while迴圈並不會跳出。那麼,是不是隻有加volatile關鍵字可以解決這個問題呢?或者說我們能不能不改動程式碼就達到目的(主執行緒中改變flag的值後,thread執行緒可以讀到,使while迴圈可以跳出)。答案當然是可以的,我們可以採用以下方式:(如圖)

  在虛擬機器引數選項上加上-Xint(請注意,這個引數在JDK1.8版本及以上),同樣能夠使while迴圈跳出,那麼這個-Xint引數到底有什麼作用呢?請看以下截圖:

  這張截圖來自Oracle的Java HotSpot VM Options 官方文件,翻譯過來的意思是:“以純解釋模式執行應用程式。禁用編譯到本機程式碼,所有位元組碼由直譯器執行。在這種模式下,just in time (JIT)編譯器所提供的效能優勢並不存在。”這麼說只要是禁止了JIT即時編譯,就起到了和加volatile一樣的作用,那他們兩個有什麼區別嗎?JIT即時編譯又做了什麼呢?請繼續往下看。

  要扯明白上面的問題,我們還要說下Java的記憶體模型,首先什麼是記憶體模型呢?周所周知,在現代計算機硬體系統的不斷改進中,CPU和記憶體之間的多級快取機制導致的快取一致性問題,以及為了高效執行程式碼而進行的處理器優化和指令重排序問題,是併發程式設計中的可見性、原子性、有序性問題的硬體層面原因。在併發程式設計中,為了保證共享記憶體的正確性(可見性、有序性、原子性),記憶體模型定義了共享記憶體系統中多執行緒程式讀寫操作行為的規範。通過這些規則來規範對記憶體的讀寫操作,從而保證指令執行的正確性。它與處理器有關、與快取有關、與併發有關、與編譯器也有關。他解決了CPU多級快取、處理器優化、指令重排等導致的記憶體訪問問題,保證了併發場景下的一致性、原子性和有序性。

  說的直白點,記憶體模型就是解決多執行緒場景下併發問題的一個重要規範,而不同的程式語言對於這個規範,在實現上可能有所不同,而Java記憶體模型(Java Memory Model ,JMM)就是Java程式語言提供的一種符合記憶體模型規範的,遮蔽了各種硬體和作業系統的訪問差異的,保證了Java程式在各種平臺下對記憶體的訪問都能保證效果一致的機制及規範。同時Java中提供了一系列和併發處理相關的關鍵字,比如volatilesynchronized等,其實這些關鍵字就是對Java記憶體模型規範的一種實現,他們封裝了Java記憶體模型規範底層的實現後提供給程式設計師使用,用來解決Java併發程式設計問題。

  有了上面的對於記憶體模型的描述,那麼我們就很好理解了,如果要解決併發程式設計中的問題,最簡單直接的做法就是不使用處理器執行程式碼的優化技術、不使用指令重排序、不使用CPU快取等等優化技術。但是,這麼做顯然就是因噎廢食了。而我們使用的-Xint引數,根據官網的的描述(沒有JIT編譯的部分,全部是由直譯器解釋執行)和最終的執行結果看,我們可以推論出:-Xint引數的作用在廢止JIT即時編譯應該也就是廢除了指令重排序和CPU快取等優化技術,這裡我沒有深入的研究過這個引數和JIT,只是臨時應用,所以做出的推論完全是根據官網的的描述和程式碼的執行結果看,不一定完全正確,如果有了解的大神,還請不吝賜教。

  那下面我們就來看看volatile關鍵字,顯然volatile關鍵字不會和-Xint一樣因噎廢食,全面封殺優化技術,那他是怎麼做的呢?

  首先記憶體模型解決併發問題主要採用兩種方式:限制處理器優化和使用記憶體屏障,volatile作為記憶體模型規範的一種應用實現方式,自然也是實現這兩種方式。

  對於volatile變數,生成的彙編程式碼在volatile修飾的共享變數進行寫操作的時候會多出一個Lock字首的指令,將這個快取中的變數回寫到系統主存中。

  lock字首指令實際上相當於一個記憶體屏障(也稱記憶體柵欄),記憶體屏障會提供3個功能:

  1)它確保指令重排序時不會把其後面的指令排到記憶體屏障之前的位置,也不會把前面的指令排到記憶體屏障的後面;即在執行到記憶體屏障這句指令時,在它前面的操作已經全部完成;

  2)它會強制將對快取的修改操作立即寫入主存;

  3)如果是寫操作,它會導致其他CPU中對應的快取行無效。

  功能1相當於禁止指令重排序優化,解決了併發變成中有序性問題。

  功能2和3,由於他處理器的快取遵守了快取一致性協議,也會把這個變數的值從主存載入到自己的快取中。這就保證了一個volatile變數在併發程式設計中,其值在多個快取中是可見的,解決了併發變成中可見性問題。

  注意:volatile並不能解決原子性問題。

  通過以上一波操作,volatile完成了併發程式設計時解決有序性和可見性問題。

 

 補充內容:

 Java虛擬機器有3種執行方式,分別是解釋執行、混合模式和編譯執行,預設情況下處於混合模式中

編譯:位元組碼 --- jit提前編譯 -- 彙編

解釋:位元組碼 – 一段段編譯 – 彙編

混合 :– 執行的過程中,JIT編譯器生效,針對熱點程式碼進行優化

 

記憶體屏障參考資料:

https://blog.csdn.net/dd864140130/article/details/56494925

參考資料:

http://www.uucode.net/201504/jvm5

https://docs.oracle.com/javase/8/docs/technotes/tools/windows/java.html

相關文章