
前幾節你應該學習到了Thread和ThreadLocal的底層原理,在接下來的幾節中,讓我們一起來探索volatile底層原理吧!
不知道你有沒有這樣的感受:有很多工程師都很難說清楚volatile這個關鍵字的作用或者原理,比如有的人壓根不知道volatile的作用、應用場景;比如有的人也不知道什么是有序性,可見性,原子性,比如有的人可能能說上來它的作用是什么“保證有序性,可見性,無法保證原子性,”但是大多數人很難說清楚為什么能保證有序性,可見性,不能保證原子性;比如在面試的時候,你經常被面試官問到volatile的時候,回答的支支吾吾的,沒有一個清晰的思路,答不出一個滿意的答案,諸如此類的場景有很多等等……
要想弄明白這些,可不是簡單的事情,所以在接下來的《JDK原始碼成長記-并發篇》中,就一步一步帶領你來探索volatile的奧秘,來解決這些尷尬的場景,可以熟練運用和理解volatile關鍵字,
Hello Volatile
Hello Volatile
首先你要了解的第一點就是,什么時候使用volatile,這里你要記住以下兩點就可以了:
1、 多個執行緒對同一個變數有讀有寫的時候
2、 多個執行緒需要保證有序性和可見性的時候
讓我們分別來看看這兩點:
volatile第一個使用場景:多個執行緒對同一個變數有讀有寫的時候,你可以通過一個Hello Volatile的例子來理解這一點,
代碼如下:
public class HelloVolatile {
//可見性舉例
private static volatile boolean shouldRunning = true;
//一個執行緒修改后,另一個執行緒無法讀到修改后的值,執行緒之間的記憶體資料不可見
// private static boolean shouldRunning = true;
public static void main(String[] args) {
new Thread(()-> {
System.out.println("讀取到變數shouldRunning="+HelloVolatile.shouldRunning);
while(HelloVolatile.shouldRunning) {
}
System.out.println("運行結束,讀取到變數shouldRunning="+HelloVolatile.shouldRunning);
}).start();
new Thread(()-> {
try {
System.out.println("修改變數");
Thread.sleep(1000);
HelloVolatile.shouldRunning = false;
} catch (InterruptedException e) {
}
}).start();
}
}
上面的代碼很明顯可以看出來,兩個執行緒,執行緒1在while回圈中使用shouldRunning判斷是否改跳出回圈,執行緒2修改了shouldRunning,這就是典型的一讀一寫的場景,
畫張圖讓大家更好的理解下:

這種用法看上去很簡單,但是其實在很多開源框架的底層,對執行緒執行控制都是通過這種方式控制的,等學完volatile之后,我會給大家舉幾個例子的,
volatile第二個使用場景:需要保證有序性和可見性的時候,后面我們會逐漸研究這兩點,
上面的例子中,如果不加volatile修飾shouldRunning變數,執行緒2修改了值后,執行緒1是不可見的,也就不會跳出回圈,
如果要想理解有序列性,這里給大家也給大家舉一個經典的例子,在執行緒安全的單例(DLC-double check lock)的場景下,volatile很重要的作用就是保證有序性,
還有一點要提到的是,volatile既保證了有序性,也保證了可見性,并不是說HelloVolatile中沒有有序性保證,
我給大家找了SpringCloud Eureka組件中的配置管理器創建,就是使用了DCL的單例,
代碼如下:
public class ConfigurationManager {
static volatile AbstractConfiguration instance = null;
public static AbstractConfiguration getConfigInstance() {
if (instance == null) {
synchronized (ConfigurationManager.class) {
if (instance == null) {
instance = getConfigInstance(Boolean.getBoolean(DynamicPropertyFactory.DISABLE_DEFAULT_CONFIG));
}
}
}
return instance;
}
}
等學完volatile之后,我們在回頭看下這個DCL使用volatile保證有序性的,這里大家有個印象就行,
什么是有序性、可見性、原子性?
什么是有序性、可見性、原子性?
前文提到了有序性、可見性、原子性,可能有人不太清楚他們是什么意思,更不理解怎么保證的,原理是什么,
而你要想理解volatile如何保證有序性和可見性,首先需要明白有序性、可見性、原子性分別是什么,這里我先不深入講解,先用一句話大白話簡單給大家概況下,
- 可見性,一句話講就是多個執行緒中有讀有寫操作同一個變數的時候,執行緒間可以互相知道,可見的意思,
- 有序性,一句話講就是由于代碼執行順序可能被重排序,volatile可以保證代碼行數按順序執行,
- 原子性,一句話講就是當多個執行緒進行同時寫同一個變數的時候,只能有一個執行緒進這一操作,
你可能看了上面三句話,還不是很明白,沒關系,最后學習完volatile了,你可以回來再看看這三句話,
下面我們從淺入深來探索下這三點,主要層次有如下幾個級別:
1、JVM記憶體模型和Java記憶體模型(JMM) 層面
2、JVM指令層面和JVM中的C++原始碼層面
3、CPU快取模型+硬體結構原理+CPU指令層面
JVM記憶體結構和JMM的概念回顧
JVM記憶體結構和JMM的概念回顧
簡單的講,一句話:就是重繪主記憶體,強制過期其他執行緒的作業記憶體,
這句話中的主記憶體和作業記憶體是Java記憶體模型中的概念,要想理解Java記憶體模型(JMM),一定要知道JVM的記憶體結構(運行時記憶體區域),下面通過幾張圖,讓你回顧下JVM記憶體結構和JMM的概念,
首先回顧一下,JVM的記憶體結構,如下圖所示:

上面的這個圖如果了解過JVM的同學,一定很熟悉了,不了解的也沒有關系,這里簡單介紹下,你就可以了解了:
JVM的記憶體區域,或者說是運行時資料區,簡單地來說分為堆和堆疊兩種區域每個執行緒共享的區域除了堆記憶體,還有一個方法區的概念,不同JVM版本的方法區實作不同,JDK1.8方法區的實作叫MetaSpace元資料空間,用于存放加載到JVM記憶體中的類的基本資訊和資料,堆記憶體就是創建的Java物件一般都會分配到堆記憶體,Heap區域,這2個公共記憶體區域可以被所有的執行緒訪問到的,它們具體作用如下:
- 堆(Heap):執行緒共享,所有的物件實體以及陣列都要在堆上分配,回收器主要管理的物件,
- 方法區(Method Area):執行緒共享,存盤類資訊、常量、靜態變數、即時編譯器編譯后的代碼,
- 方法堆疊(JVM Stack):執行緒私有,存盤區域變數表、操作堆疊、動態鏈接、方法出口,物件指標,
- 本地方法堆疊(Native Method Stack):執行緒私有,為虛擬機使用到的Native 方法服務,如Java使用c或者c++撰寫的介面服務時,代碼在此區運行,
- 程式計數器(Program Counter Register):執行緒私有,有些文章也翻譯成PC暫存器(PC Register),同一個東西,它可以看作是當前執行緒所執行的位元組碼的行號指示器,指向下一條要執行的指令,
從顏色上可以看出,除了執行緒共享的記憶體區域,每個執行緒有自己的獨有記憶體區域,比如程式計數器、本地方法堆疊,Java方法虛擬機堆疊,這個是執行緒獨有的記憶體區域,不會被其他執行緒所訪問到的,
下面給大家簡單介紹下JMM,它邏輯模型如下圖所示:

上面的這個圖可以看出來比較抽象,這是因為JMM本身就是一種記憶體模型的抽象,并不是實際存在的結構,而是有一種對應的具體實作和具體結構,
大家都知道很多事情在計算機層面,都會進行一層抽象,比如網路的分層模型等等,而在Java中,準確說是JVM在記憶體這塊的抽象概念是JMM,即Java記憶體模型,
這個抽象可以對應到具體JVM組件或者具體的硬體組件,對應關系可以理解為下圖所示:

上面的提到的JVM記憶體結構,實際就是圖中左邊,表示和JVM的對應關系是,堆和元資料空間可以看做是主記憶體,Java方法虛擬機堆疊、程式計數器等可以看做是自己的作業記憶體,
而對應右邊的其實可以對應到CPU的L1-L3的快取、高速快取區、寫緩沖器等可以看做JMM中每個執行緒的作業記憶體,而實際的物理記憶體這些可以看做是JMM中的主記憶體,執行緒共用的區域,
從JMM層面看,volatile怎么保證可見性?
從JMM層面看,volatile怎么保證可見性?
回顧了JVM記憶體結構和JMM記憶體模型后,我們來分別從這兩個層面分析volatile怎么保證的可見性,
首先是JMM層面,在JMM中,定義一些操作和規則來保證可見性,這里我們深入的講JMM的知識,只是講下我們會用到的知識,
首先說下操作,JMM規定了8中原子性操作,用來描述主記憶體和作業存在的操作動作和操作原則,
JMM的指令
-
lock(鎖定):作用于主記憶體的變數,把一個變數標識為一條執行緒獨占狀態,
-
unlock(解鎖):作用于主記憶體變數,把一個處于鎖定狀態的變數釋放出來,釋放后的變數才可以被其他執行緒鎖定,
-
read(讀取):作用于主記憶體變數,把一個變數值從主記憶體傳輸到執行緒的作業記憶體中,以便隨后的load動作使用
-
load(載入):作用于作業記憶體的變數,它把read操作從主記憶體中得到的變數值放入作業記憶體的變數副本中,
-
use(使用):作用于作業記憶體的變數,把作業記憶體中的一個變數值傳遞給執行引擎,每當虛擬機遇到一個需要使用變數的值的位元組碼指令時將會執行這個操作,
-
assign(賦值):作用于作業記憶體的變數,它把一個從執行引擎接收到的值賦值給作業記憶體的變數,每當虛擬機遇到一個給變數賦值的位元組碼指令時執行這個操作,
-
store(存盤):作用于作業記憶體的變數,把作業記憶體中的一個變數的值傳送到主記憶體中,以便隨后的write的操作,
-
write(寫入):作用于主記憶體的變數,它把store操作從作業記憶體中一個變數的值傳送到主記憶體的變數中,
JMM的指令使用規則
- 不允許read和load、store和write操作之一單獨出現,即使用了read必須load,使用了store必須write
- 不允許執行緒丟棄他最近的assign操作,即作業變數的資料改變了之后,必須告知主存
- 不允許一個執行緒將沒有assign的資料從作業記憶體同步回主記憶體
- 一個新的變數必須在主記憶體中誕生,不允許作業記憶體直接使用一個未被初始化的變數,就是對變數實施use、store操作之前,必須經過assign和load操作
- 一個變數同一時間只有一個執行緒能對其進行lock,多次lock后,必須執行相同次數的unlock才能解鎖
- 如果對一個變數進行lock操作,會清空所有作業記憶體中此變數的值,在執行引擎使用這個變數前,必須重新load或assign操作初始化變數的值
- 如果一個變數沒有被lock,就不能對其進行unlock操作,也不能unlock一個被其他執行緒鎖住的變數
- 對一個變數進行unlock操作之前,必須把此變數同步回主記憶體
上面的規則看上去很多,其實簡單的來說,可以總結如下幾句話:
必須按這個執行,不允許缺失或亂序 read-->load-->use 、assign-->store-->write; 一個個變數的lock操作,在同一時間內只允許一個執行緒重復執行多次,并且只有執行相同次數的unlock該變數才能被釋放;釋放鎖unlock之前將最新資料寫入主記憶體,進入鎖lock之前將最新資料讀入作業記憶體,
注意這8個操作,實際在CPU和JVM實作的指令層面并不完全對應,后面我們分析到JVM指令的時候會看到,他們這些,
JMM記憶體模型解釋Hello Volatile如下圖所示:

通過JMM的一些操作和原則,使用volatile就能保證不同執行緒的作業記憶體發送讀寫時候的變數可見性,
volatile保證可見性的原理,還是之前總結的一句話:寫入主記憶體資料時,重繪主記憶體值之后,強制過期其他執行緒的作業記憶體,底層是因為lock、unlock操作的原則導致的,其他執行緒讀取變數的時候必須重新加載主記憶體的最新資料,從而保證了可見性,
好了,到這里你應該了解了volatile的基本作用和可見性的原理,了解了JMM和JVM和volatile之間的關系,
下一節我們繼續深入研究下,在JVM指令層面和C++代碼層面,如何通過記憶體屏障、CPU的lock前綴指令,保證可見性和有序性的,
本文由博客群發一文多發等運營工具平臺 OpenWrite 發布
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/329985.html
標籤:Java
