摘要:一言以蔽之,被修飾的變量能夠保證每個(gè)線程能夠獲取該變量的最新值,從而避免出現(xiàn)數(shù)據(jù)臟讀的現(xiàn)象。為了實(shí)現(xiàn)內(nèi)存語義時(shí),編譯器在生成字節(jié)碼時(shí),會(huì)在指令序列中插入內(nèi)存屏障來禁止特定類型的處理器重排序。
volatile原理volatile簡介Java內(nèi)存模型告訴我們,各個(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ì)普通變量是沒有規(guī)定的,而針對(duì)volatile修飾的變量給Java 虛擬機(jī)特殊的約定,線程對(duì) volatile變量的修改會(huì)立刻被其他線程所感知,即不會(huì)出現(xiàn)數(shù)據(jù)臟讀的現(xiàn)象,從而保證數(shù)據(jù)的“可見性”。
一言以蔽之,被volatile修飾的變量能夠保證每個(gè)線程能夠獲取該變量的最新值,從而避免出現(xiàn)數(shù)據(jù)臟讀的現(xiàn)象。
volatile實(shí)現(xiàn)原理volatile是怎樣實(shí)現(xiàn)了?比如一個(gè)很簡單的Java代碼:
instance = new Instancce() //instance是volatile變量
在生成匯編代碼時(shí)會(huì)在volatile修飾的共享變量進(jìn)行寫操作的時(shí)候會(huì)多出Lock前綴的指令。 我們想這個(gè)Lock指令肯定有神奇的地方,那么Lock前綴的指令在多核處理器下會(huì)發(fā)現(xiàn)什么事情了?主要有這兩個(gè)方面的影響:
將當(dāng)前處理器緩存行的數(shù)據(jù)寫回系統(tǒng)內(nèi)存
這個(gè)寫回內(nèi)存的操作會(huì)使得其他CPU里緩存了該內(nèi)存地址的數(shù)據(jù)無效
為了提高處理速度,處理器不直接和內(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ì)有問題。
在多處理器下,為了保證各個(gè)處理器的緩存是一致的,就會(huì)實(shí)現(xiàn)緩存一致性協(xié)議,每個(gè)處理器通過嗅探在總線上傳播的數(shù)據(jù)來檢查自己緩存的值是不是過期了,當(dāng)處理器發(fā)現(xiàn)自己緩存行對(duì)應(yīng)的內(nèi)存地址被修改,就會(huì)將當(dāng)前處理器的緩存行設(shè)置成無效狀態(tài),當(dāng)處理器對(duì)這個(gè)數(shù)據(jù)進(jìn)行修改操作的時(shí)候,會(huì)重新從系統(tǒng)內(nèi)存中把數(shù)據(jù)讀到處理器緩存里。 因此,經(jīng)過分析我們可以得出如下結(jié)論:
Lock前綴的指令會(huì)引起處理器緩存寫回內(nèi)存
一個(gè)處理器的緩存回寫到內(nèi)存會(huì)導(dǎo)致其他工作內(nèi)存中的緩存失效
當(dāng)處理器發(fā)現(xiàn)本地緩存失效后,就會(huì)從主內(nèi)存中重讀該變量數(shù)據(jù),即可以獲取當(dāng)前最新值
這樣volatile變量通過這樣的機(jī)制就使得每個(gè)線程都能獲得該變量的最新值。
volatile的happens-before關(guān)系happens-before中的volatile 變量規(guī)則(Volatile Variable Rule):對(duì)一個(gè) volatile 變量的寫操作先行發(fā)生于后面對(duì)這個(gè)變量的讀操作。
public class VolatileExample {
private int a = 0;
private volatile boolean flag = false;
public void writer(){
a = 1; //1
flag = true; //2
}
public void reader(){
if(flag){ //3
int i = a; //4
}
}
}
對(duì)應(yīng)的happens-before關(guān)系如下:
加鎖線程A先執(zhí)行writer方法,然后線程B執(zhí)行reader方法。 圖中每一個(gè)箭頭兩個(gè)節(jié)點(diǎn)就代碼一個(gè)happens-before關(guān)系:
黑色的代表根據(jù)程序順序規(guī)則推導(dǎo)出來
紅色的是根據(jù)volatile變量的寫happens-before 于任意后續(xù)對(duì)volatile變量的讀
藍(lán)色的就是根據(jù)傳遞性規(guī)則推導(dǎo)出來的 這里的2 happen-before 3,同樣根據(jù)happens-before規(guī)則定義: 如果A happens-before B,則A的執(zhí)行結(jié)果對(duì)B可見,并且A的執(zhí)行順序先于B的執(zhí)行順序, 我們可以知道操作2執(zhí)行結(jié)果對(duì)操作3來說是可見的,也就是說當(dāng)線程A將volatile變量 flag更改為true后線程B就能夠迅速感知。
volatile的內(nèi)存語義public class VolatileExample {
private int a = 0;
private volatile boolean flag = false;
public void writer(){
a = 1; //1
flag = true; //2
}
public void reader(){
if(flag){ //3
int i = a; //4
}
}
}
假設(shè)線程A先執(zhí)行writer方法,線程B隨后執(zhí)行reader方法,初始時(shí)線程的本地內(nèi)存中flag和a都是初始狀態(tài),下圖是線程A執(zhí)行volatile寫后的狀態(tài)圖:
當(dāng)volatile變量寫后,線程B中本地內(nèi)存中共享變量就會(huì)置為失效的狀態(tài),因此線程B需要從主內(nèi)存中去讀取該變量的最新值。下圖就展示了線程B讀取同一個(gè)volatile變量的內(nèi)存變化示意圖:
從橫向來看,線程A和線程B之間進(jìn)行了一次通信,線程A在寫volatile變量時(shí),實(shí)際上就像是給B發(fā)送了一個(gè)消息告訴線程B你現(xiàn)在的值都是舊的了,然后線程B讀這個(gè)volatile變量時(shí)就像是接收了線程A剛剛發(fā)送的消息。既然是舊的了,那線程B該怎么辦了?自然而然就只能去主內(nèi)存去取啦。
volatile的內(nèi)存語義實(shí)現(xiàn)為了性能優(yōu)化,JMM在不改變正確語義的前提下,會(huì)允許編譯器和處理器對(duì)指令序列進(jìn)行重排序,那如果想阻止重排序要怎么辦了? 答案是可以添加內(nèi)存屏障。
四類JMM內(nèi)存屏障:
Java編譯器會(huì)在生成指令系列時(shí)在適當(dāng)?shù)奈恢脮?huì)插入內(nèi)存屏障指令來禁止特定類型的處理器重排序。 為了實(shí)現(xiàn)volatile的內(nèi)存語義,JMM會(huì)限制特定類型的編譯器和處理器重排序,JMM會(huì)針對(duì)編譯器制定volatile重排序規(guī)則表:
"NO"表示禁止重排序。 為了實(shí)現(xiàn)volatile內(nèi)存語義時(shí),編譯器在生成字節(jié)碼時(shí),會(huì)在指令序列中插入內(nèi)存屏障來禁止特定類型的處理器重排序。 對(duì)于編譯器來說,發(fā)現(xiàn)一個(gè)最優(yōu)布置來最小化插入屏障的總數(shù)幾乎是不可能的,為此,JMM采取了保守策略:
在每個(gè)volatile寫操作的前面插入一個(gè)StoreStore屏障
在每個(gè)volatile寫操作的后面插入一個(gè)StoreLoad屏障
在每個(gè)volatile讀操作的后面插入一個(gè)LoadLoad屏障
在每個(gè)volatile讀操作的后面插入一個(gè)LoadStore屏障
需要注意的是:volatile寫操作是在前面和后面分別插入內(nèi)存屏障,而volatile讀操作是在后面插入兩個(gè)內(nèi)存屏障。
volatile和synchronized的區(qū)別volatile本質(zhì)是告訴JVM當(dāng)前變量在寄存器(工作內(nèi)存)中是無效的,需要去主內(nèi)存重新讀?。籹ynchronized是鎖定當(dāng)前變量,只有持有鎖的線程才可以訪問該變量,其他線程都被阻塞直到該線程的變量操作完成;
volatile僅僅能使用在變量級(jí)別;synchronized則可以使用在變量、方法和類級(jí)別;
volatile僅僅能實(shí)現(xiàn)變量修改的可見性,不能保證原子性;而synchronized則可以保證變量修改的可見性和原子性;
volatile不會(huì)造成線程的阻塞;synchronized可能會(huì)造成線程的阻塞;
volatile修飾的變量不會(huì)被編譯器優(yōu)化;synchronized修飾的變量可以被編譯器優(yōu)化。
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://systransis.cn/yun/6809.html
摘要:一言以蔽之,被修飾的變量能夠保證每個(gè)線程能夠獲取該變量的最新值,從而避免出現(xiàn)數(shù)據(jù)臟讀的現(xiàn)象。為了實(shí)現(xiàn)內(nèi)存語義時(shí),編譯器在生成字節(jié)碼時(shí),會(huì)在指令序列中插入內(nèi)存屏障來禁止特定類型的處理器重排序。volatile原理volatile簡介Java內(nèi)存模型告訴我們,各個(gè)線程會(huì)將共享變量從主內(nèi)存中拷貝到工作內(nèi)存,然后執(zhí)行引擎會(huì)基于工作內(nèi)存中的數(shù)據(jù)進(jìn)行操作處理。 線程在工作內(nèi)存進(jìn)行操作后何時(shí)會(huì)寫到主內(nèi)存中...
摘要:一言以蔽之,被修飾的變量能夠保證每個(gè)線程能夠獲取該變量的最新值,從而避免出現(xiàn)數(shù)據(jù)臟讀的現(xiàn)象。為了實(shí)現(xiàn)內(nèi)存語義時(shí),編譯器在生成字節(jié)碼時(shí),會(huì)在指令序列中插入內(nèi)存屏障來禁止特定類型的處理器重排序。volatile原理volatile簡介Java內(nèi)存模型告訴我們,各個(gè)線程會(huì)將共享變量從主內(nèi)存中拷貝到工作內(nèi)存,然后執(zhí)行引擎會(huì)基于工作內(nèi)存中的數(shù)據(jù)進(jìn)行操作處理。 線程在工作內(nèi)存進(jìn)行操作后何時(shí)會(huì)寫到主內(nèi)存中...
摘要:我的是忙碌的一年,從年初備戰(zhàn)實(shí)習(xí)春招,年三十都在死磕源碼,三月份經(jīng)歷了阿里五次面試,四月順利收到實(shí)習(xí)。因?yàn)槲倚睦砗芮宄?,我的目?biāo)是阿里。所以在收到阿里之后的那晚,我重新規(guī)劃了接下來的學(xué)習(xí)計(jì)劃,將我的短期目標(biāo)更新成拿下阿里轉(zhuǎn)正。 我的2017是忙碌的一年,從年初備戰(zhàn)實(shí)習(xí)春招,年三十都在死磕JDK源碼,三月份經(jīng)歷了阿里五次面試,四月順利收到實(shí)習(xí)offer。然后五月懷著忐忑的心情開始了螞蟻金...
摘要:下面具體分析的用法及原理,涉及到內(nèi)存模型可見性重排序以及偽共享等方面。緩存的使用提高了的運(yùn)行效率,但是對(duì)于多核處理器會(huì)有一些問題。需要注意的是,用于保證一個(gè)變量的可見性,但是對(duì)于這種復(fù)合操作是無法保證原子性的。 簡介 在 Java 并發(fā)編程中,volatile 是經(jīng)常用到的一個(gè)關(guān)鍵字,它可以用于保證不同的線程共享一個(gè)變量時(shí)每次都能獲取最新的值。volatile 具有鎖的部分功能并且性能...
摘要:在之前,它是一個(gè)備受爭議的關(guān)鍵字,因?yàn)樵诔绦蛑惺褂盟占骼斫夂驮矸治龊喎Q,是后提供的面向大內(nèi)存區(qū)數(shù)到數(shù)多核系統(tǒng)的收集器,能夠?qū)崿F(xiàn)軟停頓目標(biāo)收集并且具有高吞吐量具有更可預(yù)測的停頓時(shí)間。 35 個(gè) Java 代碼性能優(yōu)化總結(jié) 優(yōu)化代碼可以減小代碼的體積,提高代碼運(yùn)行的效率。 從 JVM 內(nèi)存模型談線程安全 小白哥帶你打通任督二脈 Java使用讀寫鎖替代同步鎖 應(yīng)用情景 前一陣有個(gè)做...
閱讀 2680·2021-11-18 10:02
閱讀 3453·2021-09-22 15:50
閱讀 2373·2021-09-06 15:02
閱讀 3593·2019-08-29 16:34
閱讀 1756·2019-08-29 13:49
閱讀 1289·2019-08-29 13:29
閱讀 3652·2019-08-28 18:08
閱讀 2987·2019-08-26 11:52