您好,登錄后才能下訂單哦!
這篇文章將為大家詳細(xì)講解有關(guān)volatile的實(shí)現(xiàn)原理是什么,文章內(nèi)容質(zhì)量較高,因此小編分享給大家做個(gè)參考,希望大家閱讀完這篇文章后對(duì)相關(guān)知識(shí)有一定的了解。
Java編程語(yǔ)言允許線(xiàn)程訪問(wèn)共享變量,為了確保共享變量能夠被準(zhǔn)確和一致性的更新,線(xiàn)程應(yīng)該確保通過(guò)排他鎖單獨(dú)獲取這個(gè)變量。
這句話(huà)可能說(shuō)的比較繞,我們先來(lái)看一段代碼:
public class VolatileTest implements Runnable { private boolean flag = false; @Override public void run() { while (!flag){ } System.out.println("線(xiàn)程結(jié)束運(yùn)行..."); } public void setFlag(boolean flag) { this.flag = flag; } public static void main(String[] args) throws InterruptedException { VolatileTest v = new VolatileTest(); Thread t1 = new Thread(v); t1.start(); Thread.sleep(2000); v.setFlag(true); } }
這段代碼的運(yùn)行結(jié)果:
可以看到盡管在代碼中調(diào)用了v.setFlag(false)方法,線(xiàn)程也沒(méi)有結(jié)束運(yùn)行。這是因?yàn)樵谏厦娴拇a中,實(shí)際上是有2個(gè)線(xiàn)程在運(yùn)行,一個(gè)是main線(xiàn)程,一個(gè)是在main線(xiàn)程中創(chuàng)建的t1線(xiàn)程。因此我們可以看到在線(xiàn)程中的變量是互不可見(jiàn)的。 要理解線(xiàn)程中變量的可見(jiàn)性,我們需要先理解Java的內(nèi)存模型。
在Java中,所有的實(shí)例域、靜態(tài)變量和數(shù)組元素都存儲(chǔ)在堆內(nèi)存中,堆內(nèi)存在線(xiàn)程之間是共享的。局部變量,方法定義參數(shù)和異常數(shù)量參數(shù)是存放在Java虛擬機(jī)棧上面的。Java虛擬機(jī)棧是線(xiàn)程私有的因此不會(huì)在線(xiàn)程之間共享,它們不存在內(nèi)存可見(jiàn)性的問(wèn)題,也不受內(nèi)存模型的影響。
Java內(nèi)存模型(Java Memory Model 簡(jiǎn)稱(chēng) JMM),決定一個(gè)一個(gè)線(xiàn)程對(duì)共享變量的寫(xiě)入何時(shí)對(duì)其它線(xiàn)程可見(jiàn)。JMM定義了線(xiàn)程和主內(nèi)存之間的抽象關(guān)系:
線(xiàn)程之間共享變量存儲(chǔ)在主內(nèi)存中,每個(gè)線(xiàn)程都有一個(gè)私有的本地內(nèi)存,本地內(nèi)存中存儲(chǔ)了該線(xiàn)程共享變量的副本。本地內(nèi)存是JMM的一個(gè)抽象概率,并不真實(shí)的存在。它涵蓋了緩存,寫(xiě)緩存區(qū),寄存器以及其他的硬件和編譯優(yōu)化。
Java內(nèi)存模型的抽象概念圖如下所示:
看完了Java內(nèi)存模型的概念,我們?cè)賮?lái)看看內(nèi)存模型中主內(nèi)存是如何和線(xiàn)程本地內(nèi)存之間交互的。
主內(nèi)存和本地內(nèi)存的交互即一個(gè)變量是如何從主內(nèi)存中拷貝到本地內(nèi)存又是如何從本地內(nèi)存中回寫(xiě)到主內(nèi)存中的實(shí)現(xiàn),Java內(nèi)存模型提供了8中操作來(lái)完成主內(nèi)存和本地內(nèi)存之間的交互。它們分別如下:
<span >lock(鎖定)</span>:作用于主內(nèi)存的變量,它把一個(gè)變量標(biāo)識(shí)為一條線(xiàn)程獨(dú)占的狀態(tài)。
<span >unlock(解鎖)</span>:作用于主內(nèi)存的變量,它把一個(gè)處于鎖定狀態(tài)的變量釋放出來(lái),釋放后的變量才能被其它線(xiàn)程鎖定。
<span >read(讀?。?lt;/span>:作用于主內(nèi)存的變量,它把一個(gè)變量從主內(nèi)存?zhèn)鬏數(shù)骄€(xiàn)程的本地內(nèi)存中,以便隨后的load動(dòng)作使用。
<span >load(載入)</span>:作用于本地內(nèi)存的變量,它把read操作從主內(nèi)存中的到的變量值放入本地內(nèi)存的變量副本中。
<span >use(使用)</span>:作用于本地內(nèi)存的變量,它把本地內(nèi)存中一個(gè)變量的值傳遞給執(zhí)行引擎,每當(dāng)虛擬機(jī)遇到一個(gè)需要使用到變量值的字節(jié)碼指令時(shí)將會(huì)執(zhí)行這個(gè)操作。
<span >assign(賦值)</span>:作用于本地內(nèi)存的變量,它把一個(gè)從執(zhí)行引擎接收到的變量賦予給本地內(nèi)存的變量,每當(dāng)虛擬機(jī)遇到一個(gè)給變量賦值的字節(jié)碼指令時(shí)將會(huì)執(zhí)行這個(gè)操作。
<span >store(存儲(chǔ))</span>:作用于本地內(nèi)存的變量,它把本地內(nèi)存中的變量的值傳遞給主內(nèi)存中,以便后面的write操作使用。
<span >write(寫(xiě)入)</span>:作用于主內(nèi)存的變量,它把store操作從本地內(nèi)存中得到的變量的值放入主內(nèi)存的變量中。
從上面8種操作中,我們可以看出,當(dāng)一個(gè)變量從主內(nèi)存復(fù)制到線(xiàn)程的本地內(nèi)存中時(shí),需要順序的執(zhí)行read和load操作,當(dāng)一個(gè)變量從本地內(nèi)存同步到主內(nèi)存中時(shí),需要順序的執(zhí)行store和write操作。Java內(nèi)存模型只要求上述的2組操作是順序的執(zhí)行的,但并不要求連續(xù)執(zhí)行。比如對(duì)主內(nèi)存中的變量a 和 b 進(jìn)行訪問(wèn)時(shí),有可能出現(xiàn)的順序是read a read b load b load a。除此之外,Java內(nèi)存模型還規(guī)定了在執(zhí)行上述8種基本操作時(shí)必須滿(mǎn)足以下規(guī)則:
不允許read和load,store和write操作單獨(dú)出現(xiàn),這2組操作必須是成對(duì)的。
不允許一個(gè)線(xiàn)程丟棄它最近的assign操作。即變量在線(xiàn)程的本地內(nèi)存中改變后必須同步到主內(nèi)存中。
不允許一個(gè)線(xiàn)程無(wú)原因的把數(shù)據(jù)從線(xiàn)程的本地內(nèi)存同步到主內(nèi)存中。
不允許線(xiàn)程的本地內(nèi)存中使用一個(gè)未被初始化的變量。
一個(gè)變量在同一時(shí)刻只允許一個(gè)線(xiàn)程對(duì)其進(jìn)行l(wèi)ock操作,但是一個(gè)線(xiàn)程可以對(duì)一個(gè)變量進(jìn)行多次的lock操作,當(dāng)線(xiàn)程對(duì)同一變量進(jìn)行了多次lock操作后需要進(jìn)行同樣次數(shù)的unlock操作才能將變量釋放。
如果一個(gè)變量執(zhí)行了lock操作,則會(huì)清空本地內(nèi)存中變量的拷貝,當(dāng)需要使用這個(gè)變量時(shí)需要重新執(zhí)行read和load操作。
如果一個(gè)變量沒(méi)有執(zhí)行l(wèi)ock操作,那么就不能對(duì)這個(gè)變量執(zhí)行unlock操作,同樣也不允許unlock一個(gè)被其它線(xiàn)程執(zhí)行了lock操作的變量。也就是說(shuō)lock 和unlock操作是成對(duì)出現(xiàn)的并且是在同一個(gè)線(xiàn)程中。
對(duì)一個(gè)變量執(zhí)行unlock操作之前,必須將這個(gè)變量的值同步到主內(nèi)存中去。
大概了解了Java的內(nèi)存模型后,我們?cè)倏瓷厦娴拇a結(jié)果我們將很好理解為什么是這樣子的了。首先主內(nèi)存中flag的值是false,在t1線(xiàn)程執(zhí)行時(shí),依次執(zhí)行的操作有read、load和use操作,這個(gè)時(shí)候t1線(xiàn)程的本地內(nèi)存中flag的值也是false,線(xiàn)程會(huì)一直執(zhí)行。當(dāng)main線(xiàn)程調(diào)用v.setFlag(true)方法時(shí),main線(xiàn)程中的falg被賦值成了true,因?yàn)槭褂昧薬ssign操作,因此main線(xiàn)程中本地內(nèi)存的flag值將同步到主內(nèi)存中去,這時(shí)主內(nèi)存中的flag的值為true。但是t1線(xiàn)程沒(méi)有再次執(zhí)行read 和 load操作,因此t1線(xiàn)程中flag的值任然是false,所以t1線(xiàn)程不會(huì)終止運(yùn)行。想要正確的停止t1線(xiàn)程,只需要在flag變量前加上volatile修飾符即可,因?yàn)関olatile保證了變量的可見(jiàn)性。既然volatile在各個(gè)線(xiàn)程中是一致的,那么volatile是否能夠保證在并發(fā)情況下的安全呢?答案是否定的,因?yàn)関olatile不能保證變量的原子性。示例如下:
public class VolatileTest2 implements Runnable { private volatile int i = 0; @Override public void run() { for (int j=0;j<1000;j++) { i++; } } public int getI() { return i; } public static void main(String[] args) throws InterruptedException { VolatileTest2 v2 = new VolatileTest2(); for (int i=0;i<100;i++){ new Thread(v2).start(); } Thread.sleep(5000); System.out.println(v2.getI()); } }
這段代碼啟動(dòng)了100線(xiàn)程,每個(gè)線(xiàn)程都對(duì)i變量進(jìn)行1000次的自增操作,若果這段代碼能夠正確的運(yùn)行,那么正確的結(jié)果應(yīng)該是100000,但是實(shí)際并非如此,實(shí)際運(yùn)行的結(jié)果是少于100000的,這是因?yàn)関olatile不能保證i++這個(gè)操作的原子性。我們用javap反編譯這段代碼,截取run()方法的代碼片段如下:
public void run(); descriptor: ()V flags: ACC_PUBLIC Code: stack=3, locals=2, args_size=1 0: iconst_0 1: istore_1 2: iload_1 3: sipush 1000 6: if_icmpge 25 9: aload_0 10: dup 11: getfield #2 // Field i:I 14: iconst_1 15: iadd 16: putfield #2 // Field i:I 19: iinc 1, 1 22: goto 2 25: return
我們發(fā)現(xiàn)i++雖然只有一行代碼,但是在Class文件中卻是由4條字節(jié)碼指令組成的。從上面字節(jié)碼片段,我們很容易分析出并發(fā)失敗的原因:當(dāng)getfield指令把變量i的值取到操作棧時(shí),volatile關(guān)鍵字保證了i的值在此時(shí)的正確性,但是在執(zhí)行iconst_1和iadd指令時(shí),i的值可能已經(jīng)被其它的線(xiàn)程改變,此時(shí)再執(zhí)行putfield指令時(shí),就會(huì)把一個(gè)過(guò)期的值回寫(xiě)到主內(nèi)存中去了。由于volatile只保證了變量的可見(jiàn)性,在不符合以下規(guī)則的場(chǎng)景中,我們?nèi)稳恍枰褂面i來(lái)保證并發(fā)的正確性。
運(yùn)算結(jié)果結(jié)果并不依賴(lài)變量的當(dāng)前值,或者能夠確保只有單一的線(xiàn)程修改了變量的值
變量不需要與其他的狀態(tài)變量共同參與不變約束
在介紹volatile的禁止重排序之前,我們先來(lái)了解下什么是重排序。重排序是指編譯器和處理器為了優(yōu)化程序性能而對(duì)指令進(jìn)行重新排序的一種手段。那么重排序有哪些規(guī)則呢?不可能任何代碼都可以重排序,如果是這樣的話(huà),那么在單線(xiàn)程中,我們將不能得到明確的知道運(yùn)行的結(jié)果。重排序規(guī)則如下:
具有數(shù)據(jù)依賴(lài)性操作不能重排序,數(shù)據(jù)依賴(lài)性是指兩個(gè)操作訪問(wèn)同一個(gè)變量,如果一個(gè)操作是寫(xiě)操作,那么這兩個(gè)操作就存在數(shù)據(jù)依賴(lài)性。
as-if-serial語(yǔ)義,as-if-serial語(yǔ)義的意思是,不管怎么重排序,單線(xiàn)程的程序執(zhí)行結(jié)果是不會(huì)改變的。
既然volatile禁止重排序,那是不是重排序?qū)Χ嗑€(xiàn)程有影響呢?我們先來(lái)看下面的代碼示例
public class VolatileTest3 { int a = 0; boolean flag = false; public void write(){ a = 1; // 1 flag = true; // 2 } public void read(){ if(flag){ // 3 int i = a*a; // 4 System.out.println("i的值為:"+i); } } }
此時(shí)有2個(gè)線(xiàn)程A和B,線(xiàn)程A先執(zhí)行write()方法,雖有B執(zhí)行read()方法,在B線(xiàn)程執(zhí)行到第4步時(shí),i的結(jié)果能正確得到嗎?結(jié)論是 不一定 ,因?yàn)椴襟E1和2沒(méi)有數(shù)據(jù)依賴(lài)關(guān)系,因此編譯器和處理器可能對(duì)這2個(gè)操作進(jìn)行重排序。同樣步驟3和4也沒(méi)有數(shù)據(jù)依賴(lài)關(guān)系,編譯器和處理器也可以對(duì)這個(gè)2個(gè)操作進(jìn)行重排序,我們來(lái)看看這兩中重排序帶來(lái)的效果:
重上面圖片,這2組重排序都會(huì)破壞多線(xiàn)程的運(yùn)行結(jié)果。了解了重排序的概率和知道了重排序?qū)Χ嗑€(xiàn)程的影響,我們知道了volatile為什么需要禁止重排序,那JMM到底是如何實(shí)現(xiàn)volatile禁止重排序的呢?下面我們就來(lái)探討下JMM是如何實(shí)現(xiàn)volatile禁止重排序的。
前面提到過(guò),重排序分為編譯器重排序和處理器重排序,為了實(shí)現(xiàn)volatile內(nèi)存語(yǔ)義,JMM分別對(duì)這兩種重排序進(jìn)行了現(xiàn)在。下圖是JMM對(duì)編譯器重排序指定的volatile規(guī)則:
從上面圖中我們可以分析出:
當(dāng)?shù)谝粋€(gè)操作為volatile讀時(shí),無(wú)能第二個(gè)操作是什么,都不允許重排序。這個(gè)規(guī)則確保了volatile讀之后的操作不能重排序到volatile讀之前。
當(dāng)?shù)诙€(gè)操作為volatile寫(xiě)時(shí),無(wú)論第一個(gè)操作是什么,都不允許重排序。這個(gè)規(guī)則確保了volatile寫(xiě)之前的操作不能重排序到volatile寫(xiě)之后。
當(dāng)?shù)谝粋€(gè)操作是volatile寫(xiě),第二個(gè)操作是volatile讀時(shí),不允許重排序。
為了實(shí)現(xiàn)volatile內(nèi)存語(yǔ)義,編譯器在生成字節(jié)碼時(shí),會(huì)在指令序列中插入內(nèi)存屏障來(lái)禁止特定類(lèi)型處理器的重排序,在JMM中,內(nèi)存屏障的插入策略如下:
<font color="red">在每個(gè)volatile寫(xiě)操作之前插入一個(gè)StoreStore屏障</font>
<font color="red">在每個(gè)volatile寫(xiě)操作之后插入一個(gè)StoreLoad屏障</font>
<font color="red">在每個(gè)volatile讀操作之后插入一個(gè)LoadLoad屏障</font>
<font color="red">在每個(gè)volatile讀操作之后插入一個(gè)LoadStore屏障</font>
StoreStore屏障可以保證在volatile寫(xiě)之前,前面所有的普通讀寫(xiě)操作同步到主內(nèi)存中
StoreLoad屏障可以保證防止前面的volatile寫(xiě)和后面有可能出現(xiàn)的volatile度/寫(xiě)進(jìn)行重排序
LoadLoad屏障可以保證防止下面的普通讀操作和上面的volatile讀進(jìn)行重排序
LoadStore屏障可以保存防止下面的普通寫(xiě)操作和上面的volatile讀進(jìn)行重排序
上面的內(nèi)存屏障策略可以保證任何程序都能得到正確的volatile內(nèi)存語(yǔ)義。我們以下面代碼來(lái)分析
public class VolatileTest3 { int a = 0; volatile boolean flag = false; public void write(){ a = 1; // 1 flag = true; // 2 } public void read(){ if(flag){ // 3 int i = a*a; // 4 } } }
通過(guò)上面的示例我們分析了volatile指令的內(nèi)存屏蔽策略,但是這種內(nèi)存屏障的插入策略是非常保守的,在實(shí)際執(zhí)行時(shí),只要不改變volatile寫(xiě)/讀的內(nèi)存語(yǔ)義,編譯器可以根據(jù)具體情況來(lái)省略不必要的屏障。如下示例:
class VolatileBarrierExample { int a; volatile int v1 = 1; volatile int v2 = 2; void readAndWrite() { int i = v1; // 第一個(gè)volatile讀 int j = v2; // 第二個(gè)volatile讀 a = i + j; // 普通寫(xiě) v1 = i + 1; // 第一個(gè)volatile寫(xiě) v2 = j * 2; // 第二個(gè) volatile寫(xiě) } }
上述代碼,編譯器在生成字節(jié)碼時(shí),可能做了如下優(yōu)化
關(guān)于volatile的實(shí)現(xiàn)原理是什么就分享到這里了,希望以上內(nèi)容可以對(duì)大家有一定的幫助,可以學(xué)到更多知識(shí)。如果覺(jué)得文章不錯(cuò),可以把它分享出去讓更多的人看到。
免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。