原創(chuàng)|《菜鳥讀并發(fā)》java內(nèi)存模型之volatile深入解讀
點(diǎn)擊上方“碼農(nóng)進(jìn)階之路”,選擇“設(shè)為星標(biāo)”
回復(fù)“面經(jīng)”獲取面試資料
在閱讀本文前,請(qǐng)思考以下的面試題?
- volatile是什么?
- volatile的特性
- volatile是如何保證可見性的?
- volatile是如何保證有序性的?
- volatile可以保證原子性嗎?
- 使用volatile變量的條件是什么?
- volatile和synchronized的區(qū)別
- volatile和atomic原子類的區(qū)別是什么?
這一章主要是講解volatile的原理,在開始本文前,我們來看一張volatile的思維導(dǎo)圖,先有個(gè)直觀的認(rèn)識(shí)。
什么是volatile
目前的操作系統(tǒng)大多數(shù)都是多CPU,當(dāng)多線程對(duì)一個(gè)共享變量進(jìn)行操作時(shí),會(huì)出現(xiàn)數(shù)據(jù)一致性問題
Java編程語言允許線程訪問共享變量,那么為了確保共享變量能被準(zhǔn)確和一致的更新,線程應(yīng)該確保通過排他鎖單獨(dú)獲得這個(gè)變量,或者把這個(gè)變量聲明成volatile,可以理解volatile是輕量級(jí)的synchronized。
使用volatile可以在Java線程內(nèi)存模型確保所有線程看到這個(gè)變量的值是一致的,在多個(gè)處理器中保證了共享變量的“可見性”。
volatile兩核心三性質(zhì)
兩大核心:JMM內(nèi)存模型(主內(nèi)存和工作內(nèi)存)以及happens-before
三條性質(zhì):原子性,可見性,有序性
volatile性質(zhì)
- 保證了不同線程對(duì)這個(gè)變量進(jìn)行操作時(shí)的
可見性,即一個(gè)線程修改了某個(gè)變量的值,這新值對(duì)其他線程來說是立即可見的。(實(shí)現(xiàn)可見性) - 禁止進(jìn)行指令重排序。(
實(shí)現(xiàn)有序性) - 只能保證對(duì)單次讀/寫的原子性。i++ 這種操作不能保證原子性。(
不能實(shí)現(xiàn)原子性) - volatile不會(huì)引起上下文的切換和調(diào)度
總結(jié):volatile保證了可見性和有序性,同時(shí)可以保證單次讀/寫的原子性
相關(guān)的Cpu術(shù)語說明

什么是可見性?
在單核cpu的石器時(shí)代,我們所有的線程都是在一顆CPU上執(zhí)行,CPU緩存與內(nèi)存的數(shù)據(jù)一致性容易解決。因?yàn)樗芯€程都是操作同一個(gè)CPU的緩存,一個(gè)線程對(duì)緩存的寫,對(duì)另外一個(gè)線程來說一定是可見的。
例如在下面的圖中,線程A和線程B都是操作同一個(gè)CPU里面的緩存,所以線程A更新了變量a的值,那么線程B之后再訪問變量 a,得到的一定是 a 的最新值(線程 A 寫過的值)。
file在多核CPU的時(shí)代,每顆 CPU 都有自己的緩存,這時(shí) CPU 緩存與內(nèi)存的數(shù)據(jù)一致性就沒那么容易解決了,當(dāng)多個(gè)線程在不同的CPU上執(zhí)行時(shí),這些線程操作的是不同的CPU緩存。比如下圖中,線程A操作的是CPU-1上的緩存,而線程B操作的是CPU-2上的緩存,很明顯,這個(gè)時(shí)候線程A對(duì)變量a的操作對(duì)于線程B而言就不具備可見性了。這個(gè)就屬于硬件程序員給軟件程序員挖的“坑”。
為了提高處理速度,處理器不直接和內(nèi)存進(jìn)行通信,而是先將系統(tǒng)內(nèi)存的值讀到內(nèi)部緩存(L1,L2或者其他)后再進(jìn)行操作,但是操作完不知道何時(shí)再寫回內(nèi)存。
file從上面的分析,我們可以知道,多核的CPU緩存會(huì)導(dǎo)致的可見性問題。
volatile是如何保證可見性的
instance = new Singleton();//instance是volatile變量讓我們來看看在處理器下通過工具獲取JIT編譯器生成的匯編指令來查看對(duì)volatile進(jìn)行寫操作的時(shí)候,cpu會(huì)做什么事?
轉(zhuǎn)換成匯編代碼如下:
file有volatile修飾的共享變量進(jìn)行寫操作的時(shí)候會(huì)多出第二行匯編代碼,也就是jvm會(huì)向處理器發(fā)送一條Lock前綴的指令,Lock前綴的指令在多核處理器下會(huì)引發(fā)兩件事情:
- 將當(dāng)前處理器緩存行的數(shù)據(jù)寫回到系統(tǒng)內(nèi)存
- 這個(gè)寫回內(nèi)存的操作會(huì)使在其他CPU緩存了該內(nèi)存地址的數(shù)據(jù)無效,保證各個(gè)處理器的緩存是一致的 (通過一致性協(xié)議來實(shí)現(xiàn)的)
一致性協(xié):每個(gè)處理器通過嗅探在總線上傳播的數(shù)據(jù)來檢查自己的緩存的值是否過期了,當(dāng)處理器發(fā)現(xiàn)自己的緩存行對(duì)應(yīng)的內(nèi)存過期,在下次訪問相同內(nèi)存地址時(shí),強(qiáng)制執(zhí)行緩存填充,從系統(tǒng)內(nèi)存中讀取。
簡(jiǎn)單理解:volatile在其修飾的變量被線程修改時(shí),會(huì)強(qiáng)制其他線程在下一次訪問該變量時(shí)刷新緩存區(qū)。
volatile的兩條實(shí)現(xiàn)原則
Look 前綴指令會(huì)引起處理器緩存回寫到內(nèi)存。Lock 前綴指令導(dǎo)致在執(zhí)行指令期間,聲言處理器的LOCK#信號(hào)。在多處理器環(huán)境中,LOCK#信號(hào)確保在聲言該信號(hào)期間,處理器可以獨(dú)占任何共享內(nèi)存。但是,在最近的處理器里,LOCK#信號(hào)一般不鎖總線,而是鎖緩存,畢竟鎖總線開銷的比較大,對(duì)干intel486和Pentiuln處理器,在鎖操作時(shí),總是在總線上聲言LOCK#信號(hào)。但在P6和目前的處理器中,如果訪問的內(nèi)存區(qū)域已經(jīng)緩存在處理器內(nèi)部,則不會(huì)聲言LOCK#信號(hào)。相反,它會(huì)鎖定這塊內(nèi)存區(qū)域的緩存并回寫到內(nèi)存,并使用緩存一致性機(jī)制來確保修改的原子性,此操作被稱為“緩存鎖定”,緩存一致性機(jī)制會(huì)阻止同時(shí)修改由兩個(gè)以上處理器緩存的內(nèi)存區(qū)域數(shù)據(jù)。
一個(gè)處理器的緩存回寫到內(nèi)存會(huì)導(dǎo)致其他處理器的緩存無效。IA-32 處理器和 Iniel 64 處理器使用 MESI (修改、獨(dú)占、共享、無效)控制協(xié)議去維護(hù)內(nèi)部緩存和其他處理器緩存的一致性。在多核處理器系統(tǒng)中進(jìn)行操作的時(shí)候, IA-32和Intel64處理器能嗅探其他處理器訪問系統(tǒng)內(nèi)存和它們的內(nèi)部緩存。處理器使用嗅探技術(shù)保證它的內(nèi)部緩存、系統(tǒng)內(nèi)存和其他處理器的緩存的數(shù)據(jù)在總線上保持一致。例如,在 Pentium 和 P6famaly 處理器中,如果通過嗅探一個(gè)處理器來檢測(cè)其他處理器打算寫內(nèi)存地址,而這個(gè)地址當(dāng)前處干共享狀態(tài),那么正在嗅探的處理器將使它的緩存行無效,在下次訪問相同內(nèi)存地址時(shí),強(qiáng)制執(zhí)行緩存行填充
小結(jié)
- Lock前綴的指令會(huì)引起處理器緩存寫回內(nèi)存;
- 一個(gè)處理器的緩存回寫到內(nèi)存會(huì)導(dǎo)致其他處理器的緩存失效;
- 當(dāng)處理器發(fā)現(xiàn)本地緩存失效后,就會(huì)從內(nèi)存中重讀該變量數(shù)據(jù),即可以獲取當(dāng)前最新值。
volatile是如何保證有序性
在解釋有序性前,我們先來看看什么是指令重排?
導(dǎo)致程序有序性的原因是編譯優(yōu)化,指令重排序是JVM為了優(yōu)化指令,提高程序運(yùn)行效率,在不影響單線程程序執(zhí)行結(jié)果的前提下,盡可能地提高并行度。但是在多線程環(huán)境下,有些代碼的順序改變,有可能引發(fā)邏輯上的不正確。有序性最直接的辦法就是禁用緩存和編譯優(yōu)化,但是這樣問題雖然解決了,我們的程序的性能就堪憂了,所以合理的方案是按需禁用緩存或者編譯優(yōu)化。
接下來我們來看一個(gè)著名的單例模式雙重檢查鎖的實(shí)現(xiàn)
class Singleton {private volatile static Singleton instance = null;private Singleton() {}public static Singleton getInstance() {if (instance == null) { //步驟1synchronized (Singleton.class) {if (instance == null) //步驟2instance = new Singleton(); //步驟3}}return instance;}}
在以上代碼中,instance不用volatile修飾時(shí),輸出的結(jié)果會(huì)是什么呢?我們的預(yù)期中代碼是這樣子執(zhí)行的:線程A和B同時(shí)在調(diào)用getInstance()方法,線程A執(zhí)行步驟1,發(fā)現(xiàn)instance為 null,然后同步鎖住Singleton類,接著執(zhí)行步驟2再次判斷instance是否為null,發(fā)現(xiàn)仍然是null,然后執(zhí)行步驟3,開始實(shí)例化Singleton。這樣看好像沒啥毛病,可是仔細(xì)一想,發(fā)現(xiàn)事情并不簡(jiǎn)單。?這時(shí)候,我們來我們先了解一下對(duì)象是怎么初始化的?
- 對(duì)象在初始化的時(shí)候分三個(gè)步驟
memory = allocate(); //1、分配對(duì)象的內(nèi)存空間ctorInstance(memory); //2、初始化對(duì)象instance = memory; //3、使instance指向?qū)ο蟮膬?nèi)存空間
程序?yàn)榱藘?yōu)化性能,會(huì)將2和3進(jìn)行重排序,此時(shí)執(zhí)行的順序是1、3、2,在單線程中,對(duì)結(jié)果是不會(huì)有影響的,可是在多線程程序下,問題就暴露出來了。這時(shí)候我們回到剛剛的單例模式中,在實(shí)例化的過程中,線程B走到步驟1,發(fā)現(xiàn)instance不為空,但是有可能因?yàn)橹噶钪嘏帕?導(dǎo)致instance還沒有完全初始化,程序就出問題了。為了禁止實(shí)例化過程中的重排序,我們用volatile對(duì)instance修飾。
volatile內(nèi)存語義如何實(shí)現(xiàn)
對(duì)于一般的變量則會(huì)被重排序(重排序分析編譯器重排序和處理器重排序),而對(duì)于volatile則不能,這樣會(huì)影響其內(nèi)存語義,所以為了實(shí)現(xiàn)volatile的內(nèi)存語義JMM會(huì)限制重排序。
其重排序規(guī)則如下:

如果第一個(gè)操作為volatile讀,則不管第二個(gè)操作是啥,都不能重排序。這個(gè)操作確保volatile讀之后的操作不會(huì)被編譯器重排序到volatile讀之前,其前面的所有普通寫操作都已經(jīng)刷新到主內(nèi)存中;
如果第一個(gè)操作volatile寫,不管第二個(gè)操作是volatile讀/寫,禁止重排序。
如果第二個(gè)操作為volatile寫時(shí),則不管第一個(gè)操作是啥,都不能重排序。這個(gè)操作確保volatile寫之前的操作不會(huì)被編譯器重排序到volatile寫之后;
如果第二個(gè)操作為volatile讀時(shí),不管第二個(gè)操作是volatile讀/寫,禁止重排序
volatile的底層實(shí)現(xiàn)是通過插入內(nèi)存屏障,但是對(duì)于編譯器來說,發(fā)現(xiàn)一個(gè)最優(yōu)布置來最小化插入內(nèi)存屏障的總數(shù)幾乎是不可能的,所以,JMM采用了保守策略。如下:
在每一個(gè)volatile讀操作后面插入一個(gè)LoadLoad屏障,用來禁止處理器把上面的volatile讀與后面任意操作重排序
在每一個(gè)volatile寫操作前面插入一個(gè)StoreStore屏障,用來禁止volatile寫與前面任意操作重排序
在每一個(gè)volatile寫操作后面插入一個(gè)StoreLoad屏障,用來禁止volatile寫與后面可能有的volatile讀/寫操作重排序
在每一個(gè)volatile讀操作前面插入一個(gè)LoadStore屏障,用來禁止volatile寫與后面可能有的volatile讀/寫操作重排序
保守策略下,volatile的寫插入屏障后生成的指令示意圖:

Storestore 屏障可以保證在volatile寫之前,其前面的所有普通寫操作已經(jīng)對(duì)任意處理器可見了,Storestore 屏障將保障上面所有的普通寫在volatile寫之前刷新到主內(nèi)存。
這里比較有意思的是, volatite 寫后面的 StoreLoad 屏障的作用是避免volatile寫與后面可能有的volatile 讀/寫操作重排序。
因?yàn)榫幾g器常常無法準(zhǔn)確判斷在一個(gè)volatile寫的后面是否需要插入一個(gè)StoreLoad屏障。為保證能正確實(shí)現(xiàn)volatile的內(nèi)存語義,JMM在采取了保守策略,在每個(gè)volatile寫的后面,或者在每個(gè) volatile讀的前面插入一個(gè)StoreLoad屏障。
保守策略下,volatile的讀插入屏障后生成的指令示意圖:
上面的內(nèi)存屏障插入策略非常保守,在實(shí)際執(zhí)行中,只要不改變volatile寫-讀的內(nèi)存語義,編譯器可根據(jù)情況省略不必要的屏障
舉個(gè)例子:
public class Test {int a ;volatile int v1 = 1;volatile int v2 = 2;public void readWrite(){int i = v1;//第一個(gè)volatile讀int j = v2;//第二個(gè)volatile讀a = i+j://普通讀v1 = i+1;//第一個(gè)volatile寫v2 =j+2;//第二個(gè)volatile寫}public synchronized void read(){if(flag){System.out.println("---i = " + i);}}}
針對(duì)readWrite方法,編譯器在生成字節(jié)碼的時(shí)候可以做到如下的優(yōu)化:
注意:最后一個(gè)storeLoad屏障不能省略。因?yàn)榈诙€(gè)volatile寫之后,方法立即return,此時(shí)編譯器無法精準(zhǔn)判斷后面是否會(huì)有vaolatile讀或者寫。
如何正確使用volatile變量
在某些情況下,如果讀操作遠(yuǎn)遠(yuǎn)大于寫操作,volatile 變量可以提供優(yōu)于鎖的性能優(yōu)勢(shì)。
可是volatile變量不是說用就能用的,它必須滿足兩個(gè)約束條件:
- 對(duì)變量的寫操作不依賴于當(dāng)前值。
- 該變量沒有包含在具有其他變量的不變式中。
第一個(gè)條件的限制使volatile變量不能用作線程安全計(jì)數(shù)器。雖然?i++?看上去類似一個(gè)單獨(dú)操作,實(shí)際上它是一個(gè)讀取-修改-寫入三個(gè)步驟的組合操作,必須以原子方式執(zhí)行,而 volatile不能保證這種情況下的原子操作。正確的操作需要使i的值在操作期間保持不變,而volatile 變量無法做到這一點(diǎn)。
volatile和synchronized區(qū)別
- volatile比synchronized執(zhí)行成本更低,因?yàn)樗?code style="font-size:14px;font-family:'Operator Mono', Consolas, Monaco, Menlo, monospace;color:rgb(155,110,35);background-color:rgb(255,245,227);">不會(huì)引起線程上下文的切換和調(diào)度
- volatile本質(zhì)是在告訴jvm當(dāng)前變量在寄存器(工作內(nèi)存)中的值是不確定的,需要從主存中讀取;synchronized則是鎖定當(dāng)前變量,只有當(dāng)前線程可以訪問該變量,其他線程被阻塞住。
- volatile只能用來修飾變量,而synchronized可以用來修飾變量、方法、和類。
- volatile可以實(shí)現(xiàn)變量的可見性,禁止重排序和
單次讀/寫的原子性;而synchronized則可以變量的可見性,禁止重排序和原子性。 - volatile不會(huì)造成線程的阻塞;synchronized可能會(huì)造成線程的阻塞。
- volatile標(biāo)記的變量不會(huì)被編譯器優(yōu)化;synchronized標(biāo)記的變量可以被編譯器優(yōu)化。
volatile和atomic原子類區(qū)別
Volatile變量可以確保先行關(guān)系,即
寫操作會(huì)發(fā)生在后續(xù)的讀操作之前但是Volatile對(duì)
復(fù)合操作不能保證原子性。例如用volatile修飾i變量,那么i++操作就不是原子性的。atomic原子類提供的atomic方法可以讓i++這種操作具有原子性,如getAndIncrement()方法會(huì)原子性的進(jìn)行增量操作把當(dāng)前值加一,其它數(shù)據(jù)類型和引用變量也可以進(jìn)行相似操作,但是
atomic原子類一次只能操作一個(gè)共享變量,不能同時(shí)操作多個(gè)共享變量。
總結(jié)
總結(jié)一下volatile的特性``
- volatile可見性;對(duì)一個(gè)volatile的讀,總可以看到對(duì)這個(gè)變量
最終的寫volatile有序性;JVM底層采用“內(nèi)存屏障”來實(shí)現(xiàn)volatile語義volatile原子性;volatile對(duì)單個(gè)讀/寫具有原子性(32位Long、Double),但是復(fù)合操作除外,例如i++
- 《并發(fā)編程的藝術(shù)》
- 《并發(fā)編程實(shí)現(xiàn)》
?原創(chuàng)不易,歡迎轉(zhuǎn)發(fā),求個(gè)關(guān)注,文末右下角賞個(gè)"在看"吧。

往期精選
上一篇:原創(chuàng)|《菜鳥讀并發(fā)》多線程問題必知必會(huì)概念
下一篇:原創(chuàng)|《菜鳥讀并發(fā)》多線程程序如何調(diào)試?
碼農(nóng)進(jìn)階之路
長(zhǎng)按二維碼關(guān)注?
面經(jīng) | 原理?| 源碼 | 實(shí)戰(zhàn) | 工具
點(diǎn)擊留言
