volatile是一個(gè)特征修飾符,它的的作用是作為指令關(guān)鍵字茅诱,確保本條指令不會(huì)因編譯器優(yōu)化而省略逗物,且要求每次直接讀取最新值。
JMM規(guī)范介紹
Java內(nèi)存模型(Java Memory Model簡(jiǎn)稱JMM)是一種抽象的概念瑟俭,并不真實(shí)存在翎卓,它描述的是一組規(guī)則或規(guī)范,通過(guò)這組規(guī)范定義了程序中各個(gè)變量(包括實(shí)例字段摆寄,靜態(tài)字段和構(gòu)成數(shù)組對(duì)象的元素)的訪問(wèn)方式失暴。JVM運(yùn)行程序的實(shí)體是線程,而每個(gè)線程創(chuàng)建時(shí)JVM都會(huì)為其創(chuàng)建一個(gè)工作內(nèi)存(有些地方稱為椡钟空間)锐帜,用于存儲(chǔ)線程私有的數(shù)據(jù),而Java內(nèi)存模型中規(guī)定所有變量都存儲(chǔ)在主內(nèi)存畜号,主內(nèi)存是共享內(nèi)存區(qū)域缴阎,所有線程都可以訪問(wèn),但線程對(duì)變量的操作(讀取賦值等)必須在工作內(nèi)存中進(jìn)行简软,首先要將變量從主內(nèi)存拷貝的自己的工作內(nèi)存空間蛮拔,然后對(duì)變量進(jìn)行操作,操作完成后再將變量寫回主內(nèi)存痹升,不能直接操作主內(nèi)存中的變量建炫,工作內(nèi)存中存儲(chǔ)著主內(nèi)存中的變量副本拷貝。因此疼蛾,不同的線程間無(wú)法訪問(wèn)對(duì)方的工作內(nèi)存肛跌,線程間的通信(傳值)必須通過(guò)主內(nèi)存來(lái)完成。
線程,工作內(nèi)存衍慎,主內(nèi)存工作交互圖(基于JMM規(guī)范):
JMM與JVM內(nèi)存區(qū)域的劃分是不同的概念層次转唉,更恰當(dāng)?shù)恼f(shuō)JMM描述的是一組規(guī)則,通過(guò)這組規(guī)則控制程序中各個(gè)變量在共享數(shù)據(jù)區(qū)域和私有數(shù)據(jù)區(qū)域的訪問(wèn)方式稳捆,JMM是圍繞原子性赠法,有序性、可見性展開乔夯。JMM與Java內(nèi)存區(qū)域唯一相似點(diǎn)砖织,都存在共享數(shù)據(jù)區(qū)域和私有數(shù)據(jù)區(qū)域,在JMM中主內(nèi)存屬于共享數(shù)據(jù)區(qū)域末荐,從某個(gè)程度上講應(yīng)該包括了堆和方法區(qū)侧纯,而工作內(nèi)存數(shù)據(jù)線程私有數(shù)據(jù)區(qū)域,從某個(gè)程度上講則應(yīng)該包括程序計(jì)數(shù)器鞠评、虛擬機(jī)棧以及本地方法棧茂蚓。
通過(guò)上面對(duì)JMM的介紹,我們知道了各個(gè)線程會(huì)將共享變量從主內(nèi)存中拷貝到工作內(nèi)存剃幌,然后執(zhí)行引擎會(huì)基于工作內(nèi)存中的數(shù)據(jù)進(jìn)行操作處理。線程在工作內(nèi)存進(jìn)行操作后何時(shí)會(huì)寫到主內(nèi)存中晾浴?這個(gè)時(shí)機(jī)對(duì)普通變量是沒(méi)有規(guī)定的负乡,而針對(duì)volatile修飾的變量給java虛擬機(jī)特殊的約定,線程對(duì)volatile變量的修改會(huì)立刻被其他線程所感知脊凰,即不會(huì)出現(xiàn)數(shù)據(jù)臟讀的現(xiàn)象抖棘,從而保證數(shù)據(jù)的“可見性”。
volatile 的特性
- 保證了不同線程對(duì)這個(gè)變量進(jìn)行操作時(shí)的可見性狸涌,即一個(gè)線程修改了某個(gè)變量的值切省,這新值對(duì)其他線程來(lái)說(shuō)是立即可見的。(實(shí)現(xiàn)可見性)
- 禁止進(jìn)行指令重排序帕胆。(實(shí)現(xiàn)有序性)
- volatile 只能保證對(duì)單次讀/寫的原子性朝捆。i++ 這種操作不能保證原子性。
volatile 的實(shí)現(xiàn)原理
可見性
為了提高處理速度懒豹,處理器不直接和內(nèi)存進(jìn)行通信芙盘,而是先將系統(tǒng)內(nèi)存的數(shù)據(jù)讀到內(nèi)部緩存(L1,L2或其他)后再進(jìn)行操作脸秽,但操作完不知道何時(shí)會(huì)寫到內(nèi)存儒老。如果對(duì)聲明了 volatile的變量進(jìn)行寫操作,JVM就會(huì)向處理器發(fā)送一條Lock前綴的指令记餐,將這個(gè)變量所在緩存行的數(shù)據(jù)寫回到系統(tǒng)內(nèi)存驮樊。但是,就算寫回到內(nèi)存,如果其他處理器緩存的值還是舊的囚衔,再執(zhí)行計(jì)算操作就會(huì)有問(wèn)題挖腰。所以,在多處理器下佳魔,為了保證各個(gè)處理器的緩存是一致的曙聂,就會(huì)實(shí)現(xiàn)緩存一致性協(xié)議(MESI),每個(gè)處理器通過(guò)嗅探在總線上傳播的數(shù)據(jù)來(lái)檢查自己緩存的值是不是過(guò)期了鞠鲜,當(dāng)處理器發(fā)現(xiàn)自己緩存行對(duì)應(yīng)的內(nèi)存地址被修改宁脊,就會(huì)將當(dāng)前處理器的緩存行設(shè)置成無(wú)效狀態(tài),當(dāng)處理器對(duì)這個(gè)數(shù)據(jù)進(jìn)行修改操作的時(shí)候贤姆,會(huì)重新從系統(tǒng)內(nèi)存中把數(shù)據(jù)讀到處理器緩存里榆苞。
因此,經(jīng)過(guò)分析我們可以得出如下結(jié)論:
- Lock前綴的指令會(huì)引起處理器緩存寫回內(nèi)存霞捡;
- 一個(gè)處理器的緩存回寫到內(nèi)存會(huì)導(dǎo)致其他處理器的緩存失效坐漏;
- 當(dāng)處理器發(fā)現(xiàn)本地緩存失效后,就會(huì)從內(nèi)存中重讀該變量數(shù)據(jù)碧信,即可以獲取當(dāng)前最新值赊琳。
這樣針對(duì)volatile變量通過(guò)這樣的機(jī)制就使得每個(gè)線程都能獲得該變量的最新值。
線程操作數(shù)據(jù)流程圖
有序性
Java內(nèi)存模型具備一些先天的“有序性”砰碴,即不需要通過(guò)任何手段就能夠得到保證的有序性躏筏,這個(gè)通常也稱為happens-before 原則。如果兩個(gè)操作的執(zhí)行次序無(wú)法從happens-before原則推導(dǎo)出來(lái)呈枉,那么它們就不能保證它們的有序性趁尼,虛擬機(jī)可以隨意地對(duì)它們進(jìn)行重排序。
指令重排序:JVM能根據(jù)處理器特性(CPU多級(jí)緩存系統(tǒng)猖辫、多核處理器等)適當(dāng)?shù)膶?duì)機(jī)器指令進(jìn)行重排序酥泞,使機(jī)器指令能更符合CPU的執(zhí)行特性,最大限度的發(fā)揮機(jī)器性能啃憎。
JVM中提供了四類內(nèi)存屏障來(lái)禁止指令重排優(yōu)化
屏障類型 | 指令示例 | 說(shuō)明 |
---|---|---|
LoadLoad | Load1; LoadLoad; Load2 | 保證load1的讀取操作在load2及后續(xù)讀取操作之前執(zhí)行 |
StoreStore | Store1; StoreStore; Store2 | 在store2及其后的寫操作執(zhí)行前芝囤,保證store1的寫操作已刷新到主內(nèi)存 |
LoadStore | Load1; LoadStore; Store2 | 在stroe2及其后的寫操作執(zhí)行前,保證load1的讀操作已讀取結(jié)束 |
StoreLoad | Store1; StoreLoad; Load2 | 保證store1的寫操作已刷新到主內(nèi)存之后荧飞,load2及其后的讀操作才能執(zhí)行 |
內(nèi)存屏障凡人,又稱內(nèi)存柵欄,是一個(gè)CPU指令叹阔,它的作用有兩個(gè)挠轴,一是保證特定操作的執(zhí)行順序,二是保證某些變量的內(nèi)存可見性(利用該特性實(shí)現(xiàn)volatile的內(nèi)存可見性)耳幢。由于編譯器和處理器都能執(zhí)行指令重排優(yōu)化岸晦。如果在指令間插入一條Memory Barrier則會(huì)告訴編譯器和CPU欧啤,不管什么指令都不能和這條Memory Barrier指令重排序,也就是說(shuō)通過(guò)插入內(nèi)存屏障禁止在內(nèi)存屏障前后的指令執(zhí)行重排序優(yōu)化启上。Memory Barrier的另外一個(gè)作用是強(qiáng)制刷出各種CPU的緩存數(shù)據(jù)邢隧,因此任何CPU上的線程都能讀取到這些數(shù)據(jù)的最新版本「栽冢總之倒慧,volatile變量正是通過(guò)內(nèi)存屏障實(shí)現(xiàn)其在內(nèi)存中的語(yǔ)義,即可見性和禁止重排優(yōu)化包券。
JMM針對(duì)編譯器制定的volatile重排序規(guī)則表
第一個(gè)操作 | 第二個(gè)操作:普通讀寫 | 第二個(gè)操作:volatile讀 | 第二個(gè)操作:volatile寫 |
---|---|---|---|
普通讀寫 | 可以重排 | 可以重排 | 不可以重排 |
volatile讀 | 不可以重排 | 不可以重排 | 不可以重排 |
volatile寫 | 可以重排 | 不可以重排 | 不可以重排 |
volatile寫插入內(nèi)存屏障后生成的指令序列示意圖
volatile讀插入內(nèi)存屏障后生成的指令序列示意圖
原子性
CPU緩存一致性協(xié)議(MESI)為了保證高效緩存的數(shù)據(jù)一致性纫谅,它將讀到的每個(gè)緩存行(cache line 一般為64Bytes)使用4種狀態(tài)進(jìn)行標(biāo)記(使用額外的兩位(bit)表示),分別是修改(modify)溅固、獨(dú)占(exclusive)付秕、共享(shared)、失效(invalid)侍郭。
雖然有MESI協(xié)議可以保證緩存一致性询吴,但是如果有一個(gè)線程在正要進(jìn)行+1的時(shí)候被掛起了,而另一個(gè)線程則正好執(zhí)行完了x+=1的操作亮元,此時(shí)回到第一個(gè)線程繼續(xù)執(zhí)行(該變量已經(jīng)被寄存器讀取了猛计,此時(shí)并不會(huì)再次讀取內(nèi)存),這樣就會(huì)導(dǎo)致一個(gè)錯(cuò)誤的數(shù)據(jù)爆捞。因此有滑,volatile只能保證內(nèi)存的可見性,無(wú)法保證原子性的問(wèn)題嵌削。
所以在多線程中的單例模式都會(huì)在獲取實(shí)例的方法上加上一個(gè)synchronized關(guān)鍵字,以確保只會(huì)生成一個(gè)對(duì)象望艺。