如何理解volatile關(guān)鍵字的使用場(chǎng)景及其原理

本篇內(nèi)容主要講解“如何理解volatile關(guān)鍵字的使用場(chǎng)景及其原理”,感興趣的朋友不妨來看看。本文介紹的方法操作簡(jiǎn)單快捷,實(shí)用性強(qiáng)。下面就讓小編來帶大家學(xué)習(xí)“如何理解volatile關(guān)鍵字的使用場(chǎng)景及其原理”吧!

創(chuàng)新互聯(lián)自2013年創(chuàng)立以來,先為丘北等服務(wù)建站,丘北等地企業(yè),進(jìn)行企業(yè)商務(wù)咨詢服務(wù)。為丘北企業(yè)網(wǎng)站制作PC+手機(jī)+微官網(wǎng)三網(wǎng)同步一站式服務(wù)解決您的所有建站問題。

一、 Java 線程的內(nèi)存工作模型

在當(dāng)前的Java內(nèi)存模型下(JVM 1.2之后),線程可以把變量保存在本地內(nèi)存(比如機(jī)器的寄存器)中,而不是直接在主存中進(jìn)行讀寫。如圖:

 如何理解volatile關(guān)鍵字的使用場(chǎng)景及其原理

1.1 我們來看一下例子

如何理解volatile關(guān)鍵字的使用場(chǎng)景及其原理

當(dāng) signal 為false時(shí) , run 方法會(huì)終止。  上訴代碼能否實(shí)現(xiàn)我們想要的效果。

我們來看執(zhí)行結(jié)果:

如何理解volatile關(guān)鍵字的使用場(chǎng)景及其原理

分析:

如何理解volatile關(guān)鍵字的使用場(chǎng)景及其原理

從橫向去看看,線程A和線程B就好像通過共享變量在進(jìn)行隱式通信。 如果線程A更新后數(shù)據(jù)并沒有及時(shí)通知線程B,而此時(shí)線程B讀到的是過期的數(shù)據(jù)。也就是發(fā)生了緩解數(shù)據(jù)不一致的情況。  

如何解決?

可以通過同步機(jī)制(控制不同線程間操作發(fā)生的相對(duì)順序)來解決或者通過volatile關(guān)鍵字使得每次volatile變量都能夠強(qiáng)制刷新到主存,從而對(duì)每個(gè)線程都是可見的。volatile相較與同步機(jī)制會(huì)更輕量,性能更好。

修改代碼:

如何理解volatile關(guān)鍵字的使用場(chǎng)景及其原理

可以得出我們想要的結(jié)果:

如何理解volatile關(guān)鍵字的使用場(chǎng)景及其原理

二、volatile底層原理

volatile從內(nèi)存語(yǔ)義上來看:

當(dāng)寫一個(gè)volatile變量時(shí),JMM會(huì)把該線程對(duì)應(yīng)的本地內(nèi)存中的共享變量刷新到主內(nèi)存

當(dāng)讀一個(gè)volatile變量時(shí),線程接下來將從主內(nèi)存中讀取共享變量。

那底層的實(shí)現(xiàn)原理是什么?

2.1 首先,查看字節(jié)碼(javac \ javap)

如何理解volatile關(guān)鍵字的使用場(chǎng)景及其原理

然后再編譯成匯編語(yǔ)言(hsdis)

Java -XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly SingleInstance

親們實(shí)在看不懂,只能通過比較下有關(guān)鍵字volatile與沒有的差異。

可以發(fā)現(xiàn)多出來好多 lock addl

如何理解volatile關(guān)鍵字的使用場(chǎng)景及其原理

這是個(gè)啥?

2.2內(nèi)存屏障

內(nèi)存屏障(Memory Barrier)與 內(nèi)存柵欄(intel稱之為 Memory Fence)是同一個(gè)概念,不同的叫法??梢酝ㄟ^插入內(nèi)存屏障指令來禁止特定類型的處理器重排序。

volatile的底層實(shí)現(xiàn)是通過插入內(nèi)存屏障,JMM采用保守策略。如下:

在每一個(gè)volatile寫操作前面插入一個(gè)StoreStore屏障

在每一個(gè)volatile寫操作后面插入一個(gè)StoreLoad屏障

在每一個(gè)volatile讀操作后面插入一個(gè)LoadLoad屏障

在每一個(gè)volatile讀操作后面插入一個(gè)LoadStore屏障

StoreStore屏障可以保證在volatile寫之前,其前面的所有普通寫操作都已經(jīng)刷新到主內(nèi)存中;

StoreLoad屏障的作用是避免volatile寫與后面可能有的volatile讀/寫操作重排序;

LoadLoad屏障用來禁止處理器把上面的volatile讀與下面的普通讀重排序;

LoadStore屏障用來禁止處理器把上面的volatile讀與下面的普通寫重排序;

2.3指令重排序

在執(zhí)行程序時(shí)為了提高性能,編譯器和處理器常常會(huì)對(duì)指令做重排序。

指令重排序的目的是為了提高性能,指令重排序僅保證在單線程下不會(huì)改變最終的執(zhí)行結(jié)果,但無法保證在多線程下的執(zhí)行結(jié)果。

從java源代碼到最終實(shí)際執(zhí)行的指令序列,會(huì)分別經(jīng)歷下面三種重排序:

如何理解volatile關(guān)鍵字的使用場(chǎng)景及其原理

上述的1屬于編譯器重排序,2和3屬于處理器重排序。這些重排序都可能會(huì)導(dǎo)致多線程程序出現(xiàn)內(nèi)存可見性問題。

對(duì)于編譯器,JMM的編譯器重排序規(guī)則會(huì)禁止特定類型的編譯器重排序(不是所有的編譯器重排序都要禁止)。

對(duì)于處理器重排序,JMM的處理器重排序規(guī)則會(huì)要求java編譯器在生成指令序列時(shí),插入特定類型的內(nèi)存屏障指令,通過內(nèi)存屏障指令來禁止特定類型的處理器重排序(不是所有的處理器重排序都要禁止)。

JMM屬于語(yǔ)言級(jí)的內(nèi)存模型,它確保在不同的編譯器和不同的處理器平臺(tái)之上,通過禁止特定類型的編譯器重排序和處理器重排序,為程序員提供一致的內(nèi)存可見性保證。 

2.4 程序員密切相關(guān)的happens-before規(guī)則

從JDK5開始,java使用新的JSR -133內(nèi)存模型(本文除非特別說明,針對(duì)的都是JSR- 133內(nèi)存模型)。JSR-133使用happens-before的概念來闡述操作之間的內(nèi)存可見性。在JMM中,如果一個(gè)操作執(zhí)行的結(jié)果需要對(duì)另一個(gè)操作可見,那么這兩個(gè)操作之間必須要存在happens-before關(guān)系。這里提到的兩個(gè)操作既可以是在一個(gè)線程之內(nèi),也可以是在不同線程之間。

程序順序規(guī)則:

一個(gè)線程中的每個(gè)操作,happens- before 于該線程中的任意后續(xù)操作。

監(jiān)視器鎖規(guī)則:

對(duì)一個(gè)監(jiān)視器鎖的解鎖,happens- before 于隨后對(duì)這個(gè)監(jiān)視器鎖的加鎖。

volatile變量規(guī)則:對(duì)一個(gè)volatile域的寫,happens- before 于任意后續(xù)對(duì)這個(gè)volatile域的讀。

傳遞性:

如果A happens- before B,且B happens- before C,那么A happens- before C。

如何理解volatile關(guān)鍵字的使用場(chǎng)景及其原理

如上圖所示,一個(gè)happens-before規(guī)則通常對(duì)應(yīng)于多個(gè)編譯器和處理器重排序規(guī)則。對(duì)于java程序員來說,happens-before規(guī)則簡(jiǎn)單易懂,它避免java程序員為了理解JMM提供的內(nèi)存可見性保證而去學(xué)習(xí)復(fù)雜的重排序規(guī)則以及這些規(guī)則的具體實(shí)現(xiàn)。

2.5來看一個(gè)例子 -- 雙重檢測(cè)的單例

如何理解volatile關(guān)鍵字的使用場(chǎng)景及其原理

請(qǐng)問這段單例代碼有問題嗎?

分析:  

instance = new TestInstance();可以分解為3行偽代碼 

如何理解volatile關(guān)鍵字的使用場(chǎng)景及其原理

假設(shè)有線程A 執(zhí)行到 step 3, 且編譯器進(jìn)行指令重排為Step a-c-b,正好行程A剛執(zhí)行完Step c,然后線程B執(zhí)行到 step 1 , 我們來看看會(huì)發(fā)生什么?

線程B 判斷 instance==null 為false ,直接返回 instance; 而此時(shí)instance只執(zhí)行了 Step c. instance = memory //設(shè)置instance指向剛分配的地址,內(nèi)存地址中的對(duì)象尚未初始化完成。

要解決這個(gè)問題可將代碼修改為:

private volatile static SingleInstance instance = null;

三、volatile能保證原子性嗎?

看看以下描述:“volatile變量對(duì)所有線程是立即可見的,對(duì)volatile變量所有的寫操作都能立刻反應(yīng)到其他線程之中,換句話說,volatile變量在各個(gè)線程中是一致的,所以基于volatile變量的運(yùn)算在并發(fā)下是安全的”。

這句話的論據(jù)部分并沒有錯(cuò),但是其論據(jù)并不能得出“基于volatile變量的運(yùn)算在并發(fā)下是安全的”這個(gè)結(jié)論。

volatile變量在各個(gè)線程的工作內(nèi)存中不存在一致性問題,但是Java里面的運(yùn)算并非原子操作,并且volatile并不能保證原子性,導(dǎo)致volatile變量的運(yùn)算在并發(fā)下一樣是不安全的,我們可以通過一段簡(jiǎn)單的演示來說明原因,請(qǐng)看下面的例子  

如何理解volatile關(guān)鍵字的使用場(chǎng)景及其原理

輸出結(jié)果:

如何理解volatile關(guān)鍵字的使用場(chǎng)景及其原理

為什么會(huì)這樣,我們?cè)賮矸治鱿拢?/p>

如何理解volatile關(guān)鍵字的使用場(chǎng)景及其原理

再看看這段代碼的字節(jié)碼:

如何理解volatile關(guān)鍵字的使用場(chǎng)景及其原理

我們將 id++ 簡(jiǎn)單概括為三個(gè)操作:

1.讀取變量id的值;  -- volatale 保證此處跟主存一致

2.將變量id的值加1; 

3.將計(jì)算后的值再賦值給變量id的引用。

其中 2、3 不能線程安全.

想要保證原子性,可以使用請(qǐng)同步機(jī)制, 以下是采用一種原子操作的數(shù)據(jù)結(jié)構(gòu) AtomicInteger.

到此,相信大家對(duì)“如何理解volatile關(guān)鍵字的使用場(chǎng)景及其原理”有了更深的了解,不妨來實(shí)際操作一番吧!這里是創(chuàng)新互聯(lián)網(wǎng)站,更多相關(guān)內(nèi)容可以進(jìn)入相關(guān)頻道進(jìn)行查詢,關(guān)注我們,繼續(xù)學(xué)習(xí)!

標(biāo)題名稱:如何理解volatile關(guān)鍵字的使用場(chǎng)景及其原理
文章來源:http://muchs.cn/article8/piseip.html

成都網(wǎng)站建設(shè)公司_創(chuàng)新互聯(lián),為您提供網(wǎng)站維護(hù)App開發(fā)、自適應(yīng)網(wǎng)站品牌網(wǎng)站制作、網(wǎng)站建設(shè)電子商務(wù)

廣告

聲明:本網(wǎng)站發(fā)布的內(nèi)容(圖片、視頻和文字)以用戶投稿、用戶轉(zhuǎn)載內(nèi)容為主,如果涉及侵權(quán)請(qǐng)盡快告知,我們將會(huì)在第一時(shí)間刪除。文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如需處理請(qǐng)聯(lián)系客服。電話:028-86922220;郵箱:631063699@qq.com。內(nèi)容未經(jīng)允許不得轉(zhuǎn)載,或轉(zhuǎn)載時(shí)需注明來源: 創(chuàng)新互聯(lián)

成都網(wǎng)頁(yè)設(shè)計(jì)公司