在上一次https://www.cnblogs.com/webor2006/protected/p/12595201.html咱們已經對於volatile關鍵字的作用進行了一定的了解,這里回顧一下:
上一次對於第一條作用進行了詳細的解讀了,接下來則來解讀一下剩下的兩條:防止指令重排序、實現變量的可見性。而這倆其實都是通過一種手段來實現的:內存屏障(memory barrier),所以要想搞清楚這這兩條,必須得先來理解內存屏障這個概念,所以接下來重點來搞清楚內存屏障這個平常聽得比較少的這個概念。
何為指令重排序?
這其實涉及到JIT(Just In Time)的一些功能,在現代化的JVM編譯器當中,它會根據我們所寫的代碼的情況自動的一定程序的優化,其中優化當中就有一個可能就是會對咱們的指令進行一定的修改,比如按照順序執行了三條指令:1、2、3【對應我們的代碼順序】,但是在編譯完之后可能生成的字節碼會變成3、2、1,或1、3、2等,也就是對指令進行重排序了,這里用一個簡單的例子來直觀的看一下指令重排序的大概思想:
int a = 0; int b = 1; a++; 重排后可能為: int a = 0; a++; int b = 0;
對於這個重排序其實是編譯器為了讓我們的程序執行的性能更高而采取的一種優化手段,但是!!!在極端情況下這種指令重排序的優化手段並不是我們需要的,所以此時就需要防止某些指令重排序,而是按我們所編寫的代碼的順序來執行。對於指令重排序而言,在單線程環境下肯定是沒任何問題的,如果有問題也不可能出現這種優化策略了,重點是在多線程的環境下這種所謂優化的指令重排序策略可能就會產生問題,而這個volatile關鍵字就具備這種防止指令重排序的功能。
闡述內存屏障(memeory barrier):
volatile寫入操作:
這里先來看一個簡單代碼:
int a = 1; String s = "Hello"; volatile boolean v = false; //寫入操作
此時則會在volatile這句代碼之前和之后插入相應的內存屏障:
int a = 1; String s = "Hello"; 內存屏障 volatile boolean v = false; //寫入操作 內存屏障
而內存屏障是存在有分類的,這里給內存屏障再細化一下則為:
int a = 1; String s = "Hello"; 內存屏障 (Release Barrier,釋放屏障) volatile boolean v = false; //寫入操作 內存屏障(Store Barrier,存儲屏障)
很顯然這個屏障我們肉眼是看不到的,是借助於volatile來實現的,那對於這倆個屏障對於程序有啥作用呢?下面來解釋一下:
- Release Barrier:防止下面的volatile與上面的所有操作的指令重排序,並可以讓在內存屏障之前所發生的讀寫操作都能立刻的發布到所有的程序當中,其它線程就能立刻看到其修改的結果。啥意思?
- Store Barrier:它的重要作用是刷新處理器的緩存,結果是可以確保該存儲屏障之前一切的操作所生成的結果對於其他處理器來說都可見,也就是:
volatile讀取操作:
對於volatile的讀操作其內存屏障又是不一樣的,下面來看一下:
int a = 1; String s = "Hello"; 內存屏障 (Release Barrier,釋放屏障) volatile boolean v = false; //寫入操作 內存屏障(Store Barrier,存儲屏障) boolean v1 = v; //讀取操作 int a = 1; String s = "Hello";
此時由於遇到了volatile的讀取操作,則又會產生內存屏障了,它有別於之前看到的寫入的屏障,如下:
int a = 1; String s = "Hello"; 內存屏障 (Release Barrier,釋放屏障) volatile boolean v = false; //寫入操作 內存屏障(Store Barrier,存儲屏障) 內存屏障 (Load Barrier,加載屏障) boolean v1 = v; //讀取操作 內存屏障 (Acquire Barrier,獲取屏障) int a = 1; String s = "Hello";
那這兩種屏障又有何義呢?
- Load Barrier:可以刷新處理器緩存,同步其他處理器對該volatile變量的修改結果。也就是:
- Acquire Barrier:可以防止上面的volatile讀取操作與下面的所有操作語句的指令重排序。
可以發現:
對於volatile關鍵字變量的讀寫操作,本質上都是通過內存屏障來執行的,而內存屏障兼具了如下兩方面的能力:
1、防止指令重排序。
2、實現變量內存的可見性。
所以:
最后總結一下:
1、對於讀取操作來說,volatile可以確保該操作與其后續的所有讀寫操作都不會進行指令重排序。
2、對於修改操作來說,valatile可以確保該操作與其上面的所有讀寫操作都不會進行指令重排序。
注意:
在上面的舉例中都是Java的原生數據類型:
如果是一個引用類型呢?比如說ArrayList,那對於volatile的內存屏障功效是不起作用的,為啥?因為ArrayList中的讀寫操作都不是原子的,比如讀操作,得先找到元素的地址,然后再進行讀取,但是!!如果將ArrayList的引用賦值給另一個volatile的ArrayList,這就可以確保原子操作,也就有了volatile相關的功效了。
再論volatile和鎖【面試題】:
在上一次volatile的學習中已經針對它們倆的相同與不同點做了一個闡述,回憶一下:
這里由於學到了內存屏障的知識點,所以需要再拉出來進一步闡述一下,對於synchronized代碼塊而言,對應的字節碼指令我們都知道會是如下:
monitorenter
.....
monitorexit
我們知道鎖的功能比volatile功能更強大,因為它有排他性,對於volatile它不是有指令重排序和內存可見性的功效,那鎖有木有呢?當然有,所以加上這個功效之后的鎖背后的形態就會變為:
monitorenter
內存屏障 (Acquire Barrier,獲取屏障)//刷新處理器緩存,同步其他處理器對該volatile變量的修改結果,也就是獲取最新的值
.....
內存屏障 (Release Barrier,釋放屏障)//也就是將處理結果發布出去,刷新處理器緩存
monitorexit
以上就是對於同步鎖的一個完整的形態。
總結:
1、volatile關鍵字自身的劣勢:它相比不使用volatile的變量而言,性能有損失,因為對於有volatile的變量,則每次都是會從主內存(高速緩存)中來獲取了,而如果不使用volatile的變量,則會直接從寄存器上獲取,要明白,寄存器要比內存獲取快多的,所以這個關鍵字不要爛用。
2、volatile相比鎖,優點是volatile不存在阻塞,也不會進行用戶態到內核態的切換,而鎖肯定是要阻塞且會進行用戶和內核態的切換;缺點是它不具備鎖的排它性。
通過這兩篇的總結,我覺得就已經能徹底來理清這個關鍵字的含義了,真的涉及到的概念還是很難理解的。