
前幾節(jié)你應該學習到了Thread和ThreadLocal的底層原理,在接下來的幾節(jié)中,讓我們一起來探索volatile底層原理吧!
不知道你有沒有這樣的感受:有很多工程師都很難說清楚volatile這個關鍵字的作用或者原理。比如有的人壓根不知道volatile的作用、應用場景;比如有的人也不知道什么是有序性,可見性,原子性,比如有的人可能能說上來它的作用是什么“保證有序性,可見性,無法保證原子性?!钡谴蠖鄶等撕茈y說清楚為什么能保證有序性,可見性,不能保證原子性;比如在面試的時候,你經常被面試官問到volatile的時候,回答的支支吾吾的,沒有一個清晰的思路,答不出一個滿意的答案。諸如此類的場景有很多等等……
要想弄明白這些,可不是簡單的事情。所以在接下來的《JDK源碼成長記-并發(fā)篇》中,就一步一步帶領你來探索volatile的奧秘,來解決這些尷尬的場景,可以熟練運用和理解volatile關鍵字。
Hello Volatile
<div class="output_wrapper" id="output_wrapper_id" style="width:fit-content;font-size: 16px; color: rgb(62, 62, 62); line-height: 1.6; word-spacing: 0px; letter-spacing: 0px; font-family: 'Helvetica Neue', Helvetica, 'Hiragino Sans GB', 'Microsoft YaHei', Arial, sans-serif;"><h3 id="hdddd" style="width:fit-content;line-height: inherit; margin: 1.5em 0px; font-weight: bold; font-size: 1.3em; margin-bottom: 2em; margin-right: 5px; padding: 8px 15px; letter-spacing: 2px; background-image: linear-gradient(to right bottom, rgb(43,48,70), rgb(43,48,70)); background-color: rgb(63, 81, 181); color: rgb(255, 255, 255); border-left: 10px solid rgb(255,204,0); border-radius: 5px; text-shadow: rgb(102, 102, 102) 1px 1px 1px; box-shadow: rgb(102, 102, 102) 1px 1px 2px;"><span style="font-size: inherit; color: inherit; line-height: inherit; margin: 0px; padding: 0px;">Hello Volatile</span></h3></div>
首先你要了解的第一點就是,什么時候使用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循環(huán)中使用shouldRunning判斷是否改跳出循環(huán),線程2修改了shouldRunning。這就是典型的一讀一寫的場景。
畫張圖讓大家更好的理解下:

這種用法看上去很簡單,但是其實在很多開源框架的底層,對線程執(zhí)行控制都是通過這種方式控制的。等學完volatile之后,我會給大家舉幾個例子的。
volatile第二個使用場景:需要保證有序性和可見性的時候。后面我們會逐漸研究這兩點。
上面的例子中,如果不加volatile修飾shouldRunning變量,線程2修改了值后,線程1是不可見的,也就不會跳出循環(huán)。
如果要想理解有序列性,這里給大家也給大家舉一個經典的例子,在線程安全的單例(DLC-double check lock)的場景下,volatile很重要的作用就是保證有序性。
還有一點要提到的是,volatile既保證了有序性,也保證了可見性。并不是說HelloVolatile中沒有有序性保證。
我給大家找了SpringCloud Eureka組件中的配置管理器創(chuàng)建,就是使用了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保證有序性的。這里大家有個印象就行。
什么是有序性、可見性、原子性?
<div class="output_wrapper" id="output_wrapper_id" style="width:fit-content;font-size: 16px; color: rgb(62, 62, 62); line-height: 1.6; word-spacing: 0px; letter-spacing: 0px; font-family: 'Helvetica Neue', Helvetica, 'Hiragino Sans GB', 'Microsoft YaHei', Arial, sans-serif;"><h3 id="hdddd" style="width:fit-content;line-height: inherit; margin: 1.5em 0px; font-weight: bold; font-size: 1.3em; margin-bottom: 2em; margin-right: 5px; padding: 8px 15px; letter-spacing: 2px; background-image: linear-gradient(to right bottom, rgb(43,48,70), rgb(43,48,70)); background-color: rgb(63, 81, 181); color: rgb(255, 255, 255); border-left: 10px solid rgb(255,204,0); border-radius: 5px; text-shadow: rgb(102, 102, 102) 1px 1px 1px; box-shadow: rgb(102, 102, 102) 1px 1px 2px;"><span style="font-size: inherit; color: inherit; line-height: inherit; margin: 0px; padding: 0px;">什么是有序性、可見性、原子性?</span></h3></div>
前文提到了有序性、可見性、原子性。可能有人不太清楚他們是什么意思。更不理解怎么保證的,原理是什么。
而你要想理解volatile如何保證有序性和可見性,首先需要明白有序性、可見性、原子性分別是什么。這里我先不深入講解,先用一句話大白話簡單給大家概況下。
- 可見性,一句話講就是多個線程中有讀有寫操作同一個變量的時候,線程間可以互相知道,可見的意思。
- 有序性,一句話講就是由于代碼執(zhí)行順序可能被重排序,volatile可以保證代碼行數按順序執(zhí)行。
- 原子性,一句話講就是當多個線程進行同時寫同一個變量的時候,只能有一個線程進這一操作。
你可能看了上面三句話,還不是很明白,沒關系,最后學習完volatile了,你可以回來再看看這三句話。
下面我們從淺入深來探索下這三點。主要層次有如下幾個級別:
1、JVM內存模型和Java內存模型(JMM) 層面
2、JVM指令層面和JVM中的C++源碼層面
3、CPU緩存模型+硬件結構原理+CPU指令層面
JVM內存結構和JMM的概念回顧
<div class="output_wrapper" id="output_wrapper_id" style="width:fit-content;font-size: 16px; color: rgb(62, 62, 62); line-height: 1.6; word-spacing: 0px; letter-spacing: 0px; font-family: 'Helvetica Neue', Helvetica, 'Hiragino Sans GB', 'Microsoft YaHei', Arial, sans-serif;"><h3 id="hdddd" style="width:fit-content;line-height: inherit; margin: 1.5em 0px; font-weight: bold; font-size: 1.3em; margin-bottom: 2em; margin-right: 5px; padding: 8px 15px; letter-spacing: 2px; background-image: linear-gradient(to right bottom, rgb(43,48,70), rgb(43,48,70)); background-color: rgb(63, 81, 181); color: rgb(255, 255, 255); border-left: 10px solid rgb(255,204,0); border-radius: 5px; text-shadow: rgb(102, 102, 102) 1px 1px 1px; box-shadow: rgb(102, 102, 102) 1px 1px 2px;"><span style="font-size: inherit; color: inherit; line-height: inherit; margin: 0px; padding: 0px;">JVM內存結構和JMM的概念回顧</span></h3></div>
簡單的講,一句話:就是刷新主內存,強制過期其他線程的工作內存。
這句話中的主內存和工作內存是Java內存模型中的概念,要想理解Java內存模型(JMM),一定要知道JVM的內存結構(運行時內存區(qū)域)。下面通過幾張圖,讓你回顧下JVM內存結構和JMM的概念。
首先回顧一下,JVM的內存結構,如下圖所示:

上面的這個圖如果了解過JVM的同學,一定很熟悉了,不了解的也沒有關系。這里簡單介紹下,你就可以了解了:
JVM的內存區(qū)域,或者說是運行時數據區(qū),簡單地來說分為堆和棧兩種區(qū)域每個線程共享的區(qū)域除了堆內存,還有一個方法區(qū)的概念。不同JVM版本的方法區(qū)實現不同,JDK1.8方法區(qū)的實現叫MetaSpace元數據空間,用于存放加載到JVM內存中的類的基本信息和數據。堆內存就是創(chuàng)建的Java對象一般都會分配到堆內存,Heap區(qū)域。這2個公共內存區(qū)域可以被所有的線程訪問到的。它們具體作用如下:
- 堆(Heap):線程共享。所有的對象實例以及數組都要在堆上分配?;厥掌髦饕芾淼膶ο?。
- 方法區(qū)(Method Area):線程共享。存儲類信息、常量、靜態(tài)變量、即時編譯器編譯后的代碼。
- 方法棧(JVM Stack):線程私有。存儲局部變量表、操作棧、動態(tài)鏈接、方法出口,對象指針。
- 本地方法棧(Native Method Stack):線程私有。為虛擬機使用到的Native 方法服務。如Java使用c或者c++編寫的接口服務時,代碼在此區(qū)運行。
- 程序計數器(Program Counter Register):線程私有。有些文章也翻譯成PC寄存器(PC Register),同一個東西。它可以看作是當前線程所執(zhí)行的字節(jié)碼的行號指示器。指向下一條要執(zhí)行的指令。
從顏色上可以看出,除了線程共享的內存區(qū)域,每個線程有自己的獨有內存區(qū)域,比如程序計數器、本地方法棧,Java方法虛擬機棧。這個是線程獨有的內存區(qū)域,不會被其他線程所訪問到的。
下面給大家簡單介紹下JMM。它邏輯模型如下圖所示:

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

上面的提到的JVM內存結構,實際就是圖中左邊,表示和JVM的對應關系是,堆和元數據空間可以看做是主內存,Java方法虛擬機棧、程序計數器等可以看做是自己的工作內存。
而對應右邊的其實可以對應到CPU的L1-L3的緩存、高速緩存區(qū)、寫緩沖器等可以看做JMM中每個線程的工作內存,而實際的物理內存這些可以看做是JMM中的主內存,線程共用的區(qū)域。
從JMM層面看,volatile怎么保證可見性?
<div class="output_wrapper" id="output_wrapper_id" style="width:fit-content;font-size: 16px; color: rgb(62, 62, 62); line-height: 1.6; word-spacing: 0px; letter-spacing: 0px; font-family: 'Helvetica Neue', Helvetica, 'Hiragino Sans GB', 'Microsoft YaHei', Arial, sans-serif;"><h3 id="hdddd" style="width:fit-content;line-height: inherit; margin: 1.5em 0px; font-weight: bold; font-size: 1.3em; margin-bottom: 2em; margin-right: 5px; padding: 8px 15px; letter-spacing: 2px; background-image: linear-gradient(to right bottom, rgb(43,48,70), rgb(43,48,70)); background-color: rgb(63, 81, 181); color: rgb(255, 255, 255); border-left: 10px solid rgb(255,204,0); border-radius: 5px; text-shadow: rgb(102, 102, 102) 1px 1px 1px; box-shadow: rgb(102, 102, 102) 1px 1px 2px;"><span style="font-size: inherit; color: inherit; line-height: inherit; margin: 0px; padding: 0px;"> 從JMM層面看,volatile怎么保證可見性?</span></h3></div>
回顧了JVM內存結構和JMM內存模型后,我們來分別從這兩個層面分析volatile怎么保證的可見性。
首先是JMM層面。在JMM中,定義一些操作和規(guī)則來保證可見性。這里我們深入的講JMM的知識,只是講下我們會用到的知識。
首先說下操作,JMM規(guī)定了8中原子性操作,用來描述主內存和工作存在的操作動作和操作原則。
JMM的指令
lock(鎖定):作用于主內存的變量,把一個變量標識為一條線程獨占狀態(tài)。
unlock(解鎖):作用于主內存變量,把一個處于鎖定狀態(tài)的變量釋放出來,釋放后的變量才可以被其他線程鎖定。
read(讀取):作用于主內存變量,把一個變量值從主內存?zhèn)鬏數骄€程的工作內存中,以便隨后的load動作使用
load(載入):作用于工作內存的變量,它把read操作從主內存中得到的變量值放入工作內存的變量副本中。
use(使用):作用于工作內存的變量,把工作內存中的一個變量值傳遞給執(zhí)行引擎,每當虛擬機遇到一個需要使用變量的值的字節(jié)碼指令時將會執(zhí)行這個操作。
assign(賦值):作用于工作內存的變量,它把一個從執(zhí)行引擎接收到的值賦值給工作內存的變量,每當虛擬機遇到一個給變量賦值的字節(jié)碼指令時執(zhí)行這個操作。
store(存儲):作用于工作內存的變量,把工作內存中的一個變量的值傳送到主內存中,以便隨后的write的操作。
write(寫入):作用于主內存的變量,它把store操作從工作內存中一個變量的值傳送到主內存的變量中。
JMM的指令使用規(guī)則
- 不允許read和load、store和write操作之一單獨出現。即使用了read必須load,使用了store必須write
- 不允許線程丟棄他最近的assign操作,即工作變量的數據改變了之后,必須告知主存
- 不允許一個線程將沒有assign的數據從工作內存同步回主內存
- 一個新的變量必須在主內存中誕生,不允許工作內存直接使用一個未被初始化的變量。就是對變量實施use、store操作之前,必須經過assign和load操作
- 一個變量同一時間只有一個線程能對其進行l(wèi)ock。多次lock后,必須執(zhí)行相同次數的unlock才能解鎖
- 如果對一個變量進行l(wèi)ock操作,會清空所有工作內存中此變量的值,在執(zhí)行引擎使用這個變量前,必須重新load或assign操作初始化變量的值
- 如果一個變量沒有被lock,就不能對其進行unlock操作。也不能unlock一個被其他線程鎖住的變量
- 對一個變量進行unlock操作之前,必須把此變量同步回主內存
上面的規(guī)則看上去很多,其實簡單的來說,可以總結如下幾句話:
必須按這個執(zhí)行,不允許缺失或亂序 read-->load-->use 、assign-->store-->write; 一個個變量的lock操作,在同一時間內只允許一個線程重復執(zhí)行多次,并且只有執(zhí)行相同次數的unlock該變量才能被釋放;釋放鎖unlock之前將最新數據寫入主內存,進入鎖lock之前將最新數據讀入工作內存。
注意這8個操作,實際在CPU和JVM實現的指令層面并不完全對應,后面我們分析到JVM指令的時候會看到。他們這些。
JMM內存模型解釋Hello Volatile如下圖所示:

通過JMM的一些操作和原則,使用volatile就能保證不同線程的工作內存發(fā)送讀寫時候的變量可見性。
volatile保證可見性的原理,還是之前總結的一句話:寫入主內存數據時,刷新主內存值之后,強制過期其他線程的工作內存,底層是因為lock、unlock操作的原則導致的,其他線程讀取變量的時候必須重新加載主內存的最新數據,從而保證了可見性。
好了,到這里你應該了解了volatile的基本作用和可見性的原理,了解了JMM和JVM和volatile之間的關系。
下一節(jié)我們繼續(xù)深入研究下,在JVM指令層面和C++代碼層面,如何通過內存屏障、CPU的lock前綴指令,保證可見性和有序性的。
本文由博客一文多發(fā)平臺 OpenWrite 發(fā)布!