溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點(diǎn)擊 登錄注冊 即表示同意《億速云用戶服務(wù)條款》

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

發(fā)布時間:2021-10-18 17:30:14 來源:億速云 閱讀:125 作者:iii 欄目:編程語言

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

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

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

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

1.1 我們來看一下例子

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

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

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

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

分析:

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

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

如何解決?

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

修改代碼:

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

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

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

二、volatile底層原理

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

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

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

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

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

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

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

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

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

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

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

這是個啥?

2.2內(nèi)存屏障

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

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

在每一個volatile寫操作前面插入一個StoreStore屏障

在每一個volatile寫操作后面插入一個StoreLoad屏障

在每一個volatile讀操作后面插入一個LoadLoad屏障

在每一個volatile讀操作后面插入一個LoadStore屏障

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

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

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

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

2.3指令重排序

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

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

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

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

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

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

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

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

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

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

程序順序規(guī)則:

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

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

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

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

傳遞性:

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

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

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

2.5來看一個例子 -- 雙重檢測的單例

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

請問這段單例代碼有問題嗎?

分析:  

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

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

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

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

要解決這個問題可將代碼修改為:

private volatile static SingleInstance instance = null;

三、volatile能保證原子性嗎?

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

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

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

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

輸出結(jié)果:

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

為什么會這樣,我們再來分析下:

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

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

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

我們將 id++ 簡單概括為三個操作:

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

2.將變量id的值加1; 

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

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

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

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

向AI問一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進(jìn)行舉報,并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI