Java中的volatile關(guān)鍵字原理深入解析
volatile介紹
Java 語(yǔ)言規(guī)范 volatile 關(guān)鍵字定義:Java 編程語(yǔ)言允許線程訪問(wèn)共享變量,為了確保共享變量能被準(zhǔn)確和一致地更新,線程應(yīng)該確保通過(guò)排他鎖單獨(dú)獲得這個(gè)變量。
volatile通常被比喻成"輕量級(jí)的synchronized",也是Java并發(fā)編程中比較重要的一個(gè)關(guān)鍵字。
和synchronized不同,volatile是一個(gè)變量修飾符,只能用來(lái)修飾變量。無(wú)法修飾方法及代碼塊等。
volatile的用法比較簡(jiǎn)單,只需要在聲明一個(gè)可能被多線程同時(shí)訪問(wèn)的變量時(shí),使用volatile修飾就可以了。
private volatile static Singleton singleton;
volatile的原理
每個(gè)線程有自己的工作內(nèi)存,線程的工作內(nèi)存中保存了被該線程所使用到的變量(這些變量是從主內(nèi)存中拷貝而來(lái))。線程對(duì)變量的所有操作(讀取,賦值,抹除)都必須在工作內(nèi)存中進(jìn)行。不同線程之間也無(wú)法直接訪問(wèn)對(duì)方工作內(nèi)存中的變量,線程間變量值的傳遞均需要通過(guò)主內(nèi)存來(lái)完成。
對(duì)于volatile變量,當(dāng)對(duì)volatile變量進(jìn)行寫操作的時(shí)候,JVM會(huì)向處理器發(fā)送一條lock前綴的指令,將這個(gè)緩存中的變量回寫到主存中。
但是就算寫回到主存,如果其他處理器緩存的值還是舊的,再執(zhí)行計(jì)算操作就會(huì)有問(wèn)題,所以在多處理器下,為了保證各個(gè)處理器的緩存是一致的,就會(huì)實(shí)現(xiàn)緩存一致性協(xié)議。
緩存一致性協(xié)議:每個(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ì)強(qiáng)制重新從系統(tǒng)內(nèi)存里把數(shù)據(jù)讀到處理器緩存里。
所以,如果一個(gè)變量被volatile所修飾的話,在每次數(shù)據(jù)變化之后,其值都會(huì)被強(qiáng)制刷入主存。而其他處理器的緩存由于遵守了緩存一致性協(xié)議,也會(huì)把這個(gè)變量的值從主存加載到自己的緩存中。這就保證了一個(gè)volatile在并發(fā)編程中,其值在多個(gè)緩存中是可見的。
volatile與可見性
可見性是指當(dāng)多個(gè)線程訪問(wèn)同一個(gè)變量時(shí),一個(gè)線程修改了這個(gè)變量的值,其他線程能夠立即看得到修改的值。
Java內(nèi)存模型規(guī)定了所有的變量都存儲(chǔ)在主內(nèi)存中,每條線程還有自己的工作內(nèi)存,線程的工作內(nèi)存中保存了該線程中使用到的變量的主內(nèi)存副本拷貝,線程對(duì)變量的所有操作都必須在工作內(nèi)存中進(jìn)行,而不能直接讀寫主內(nèi)存。不同的線程之間也無(wú)法直接訪問(wèn)對(duì)方工作內(nèi)存中的變量,線程間變量的傳遞均需要自己的工作內(nèi)存和主存之間進(jìn)行數(shù)據(jù)同步進(jìn)行。所以,就可能出現(xiàn)線程1改了某個(gè)變量的值,但是線程2不可見的情況。
前面的關(guān)于volatile的原理中介紹過(guò)了,Java中的volatile關(guān)鍵字提供了一個(gè)功能,那就是被其修飾的變量在被修改后可以立即同步到主內(nèi)存,被其修飾的變量在每次使用之前都從主內(nèi)存刷新。因此,可以使用volatile來(lái)保證多線程操作時(shí)變量的可見性。
JVM對(duì) volatile 的描述如下描述中寫著,如果字節(jié)碼中有用VOLATILE修飾的,代表這個(gè)變量不能被線程所緩存。
可以通過(guò)一下代碼進(jìn)行驗(yàn)證,你會(huì)發(fā)現(xiàn)該程序不會(huì)結(jié)束,但是如果你對(duì)flag加上了volatile修飾的話,結(jié)果就會(huì)不一樣了:
private static boolean flag = false; public static void main(String[] args) { new Thread(() -> { flag = true; }).start(); while (!aBoolean) { } System.out.println("end"); } private static void run()
volatile與有序性
有序性即程序執(zhí)行的順序按照代碼的先后順序執(zhí)行。
普通的變量?jī)H僅會(huì)保證在該方法的執(zhí)行過(guò)程中所依賴的賦值結(jié)果的地方都能獲得正確的結(jié)果,而不能保證變量的賦值操作的順序與程序代碼中的執(zhí)行順序一致。由于處理器優(yōu)化和指令重排等,CPU還可能對(duì)輸入代碼進(jìn)行亂序執(zhí)行,比如load->add->save 有可能被優(yōu)化成load->save->add ,這就是有序性問(wèn)題。
volatile可以禁止指令重排,這就保證了代碼的程序會(huì)嚴(yán)格按照代碼的先后順序執(zhí)行。這就保證了有序性。被volatile修飾的變量的操作,會(huì)嚴(yán)格按照代碼順序執(zhí)行,load->add->save 的執(zhí)行順序就是:load->add->save 。
重排序
為什么要有重排序呢?
簡(jiǎn)單來(lái)說(shuō),就是為了提升執(zhí)行效率。
為什么能提升執(zhí)行效率呢?重排序可以提高程序的運(yùn)行效率,但是必須遵循 as-if-serial 語(yǔ)義。as-if-serial 語(yǔ)義是什么呢?簡(jiǎn)單來(lái)說(shuō),就是不管你怎么重排序,你必須保證不管怎么重排序,單線程下程序的執(zhí)行結(jié)果不能被改變(至于多線程就管不到了)。
內(nèi)存屏障
如何保證CPU不會(huì)對(duì)這些操作進(jìn)行重排序呢?
JVM是通過(guò)插入內(nèi)存屏障保證的,JMM 規(guī)范中定義的內(nèi)存屏障分為讀(load)屏障和寫(Store)屏障,排列組合就有了四種屏障。對(duì)于 volatile 操作,JMM 內(nèi)存屏障插入策略:
- 在每個(gè) volatile 寫操作的前面插入一個(gè) StoreStore 屏障
- 在每個(gè) volatile 寫操作的后面插入一個(gè) StoreLoad 屏障
- 在每個(gè) volatile 讀操作的后面插入一個(gè) LoadLoad 屏障
- 在每個(gè) volatile 讀操作的后面插入一個(gè) LoadStore 屏障
而在 x86 處理器中,有三種方法可以實(shí)現(xiàn)實(shí)現(xiàn) StoreLoad 屏障的效果,分別為:
- mfence 指令:能實(shí)現(xiàn)全能型屏障,具備 lfence 和 sfence 的能力。
- cpuid 指令:cpuid 操作碼是一個(gè)面向 x86 架構(gòu)的處理器補(bǔ)充指令,它的名稱派生自 CPU 識(shí)別,作用是允許軟件發(fā)現(xiàn)處理器的詳細(xì)信息。
- lock 指令前綴:總線鎖。lock 前綴只能加在一些特殊的指令前面。
lock指令
實(shí)際上 HotSpot 關(guān)于 volatile 的實(shí)現(xiàn)就是使用的 lock 指令,只在 volatile 標(biāo)記的地方加上帶 lock 前綴指令操作,并沒有參照 JMM 規(guī)范的屏障設(shè)計(jì)。
lock 前綴指令實(shí)際上相當(dāng)于一個(gè)內(nèi)存屏障(也成內(nèi)存柵欄),并提供以下三個(gè)功能:
- 它確保指令重排序時(shí)不會(huì)把其后面的指令排到內(nèi)存屏障之前的位置,也不會(huì)把前面的指令排到內(nèi)存屏障的后面;即在執(zhí)行到內(nèi)存屏障這句指令時(shí),在它前面的操作已經(jīng)全部完成;
- 它會(huì)強(qiáng)制將對(duì)緩存的修改操作立即寫入主存;
- 如果是寫操作,它會(huì)導(dǎo)致其他 CPU 中對(duì)應(yīng)的緩存行無(wú)效。
volatile與原子性
原子性是指一個(gè)操作是不可中斷的,要全部執(zhí)行完成,要不就都不執(zhí)行。
線程是CPU調(diào)度的基本單位。CPU有時(shí)間片的概念,會(huì)根據(jù)不同的調(diào)度算法進(jìn)行線程調(diào)度。當(dāng)一個(gè)線程獲得時(shí)間片之后開始執(zhí)行,在時(shí)間片耗盡之后,就會(huì)失去CPU使用權(quán)。所以在多線程場(chǎng)景下,由于時(shí)間片在線程間輪換,就會(huì)發(fā)生原子性問(wèn)題。
為了保證原子性,需要通過(guò)字節(jié)碼指令monitorenter和monitorexit,但是volatile和這兩個(gè)指令之間是沒有任何關(guān)系的。所以,volatile是不能保證原子性的。
在以下兩個(gè)場(chǎng)景中可以使用volatile來(lái)代替synchronized:
- 運(yùn)算結(jié)果并不依賴變量的當(dāng)前值,或者能夠確保只有單一的線程會(huì)修改變量的值。
- 變量不需要與其他狀態(tài)變量共同參與不變約束。
除以上場(chǎng)景外,都需要使用其他方式來(lái)保證原子性,如synchronized或者JUC。
我們來(lái)看一下volatile和原子性的例子:
public class Test { public volatile int inc = 0; public void increase() { inc++; } public static void main(String[] args) { final Test test = new Test(); for(int i=0;i<10;i++){ new Thread(){ public void run() { for(int j=0;j<1000;j++) test.increase(); }; }.start(); } while(Thread.activeCount()>1) //保證前面的線程都執(zhí)行完 Thread.yield(); System.out.println(test.inc); } }
以上代碼比較簡(jiǎn)單,就是創(chuàng)建10個(gè)線程,然后分別執(zhí)行1000次j++操作。正常情況下,程序的輸出結(jié)果應(yīng)該是10000,但是,多次執(zhí)行的結(jié)果都小于10000。這其實(shí)就是volatile無(wú)法滿足原子性的原因。
對(duì)于一個(gè)簡(jiǎn)單的i++操作,一共有三個(gè)步驟:load , add ,save 。共享變量就會(huì)被多個(gè)線程同時(shí)進(jìn)行操作,這樣讀改寫操作就不是原子的,操作完之后共享變量的值可以會(huì)和期望的不一致。
DCL與volatile
為什么雙重校驗(yàn)鎖實(shí)現(xiàn)的單例中,已經(jīng)使用了synchronized,為什么還需要volatile?
public class Singleton { private volatile static Singleton singleton; private Singleton (){} public static Singleton getSingleton() { if (singleton == null) { synchronized (Singleton.class) { if (singleton == null) { singleton = new Singleton(); } } } return singleton; } }
因?yàn)閟ingleton = new Singleton()不是一個(gè)原子操作,大概要經(jīng)過(guò)這幾個(gè)步驟:
- 分配一塊內(nèi)存空間
- 調(diào)用構(gòu)造器,初始化實(shí)例
- singleton指向分配的內(nèi)存空間
由于cpu重排序問(wèn)題,導(dǎo)致實(shí)際執(zhí)行步驟可能是這樣的:
- 申請(qǐng)一塊內(nèi)存空間
- singleton指向分配的內(nèi)存空間
- 調(diào)用構(gòu)造器,初始化實(shí)例
在singleton指向分配的內(nèi)存空間之后,singleton就不為空了。
但是在沒有調(diào)用構(gòu)造器初始化實(shí)例之前,這個(gè)對(duì)象還處于半初始化狀態(tài),在這個(gè)狀態(tài)下,實(shí)例的屬性都還是默認(rèn)屬性,這個(gè)時(shí)候如果有另一個(gè)線程調(diào)用getSingleton()方法時(shí),會(huì)拿到這個(gè)未初始化的對(duì)象,導(dǎo)致出錯(cuò)。 而加 volatile 修飾之后,就會(huì)禁止重排序,這樣就能保證在對(duì)象初始化完了之后才把singleton指向分配的內(nèi)存空間,杜絕了一些不可控錯(cuò)誤的產(chǎn)生。volatile 提供了 happens-before 保證,Happens-before 主要是解決前一個(gè)操作的結(jié)果必須對(duì)后一個(gè)操作可見。
到此這篇關(guān)于Java中的volatile關(guān)鍵字原理深入解析的文章就介紹到這了,更多相關(guān)volatile關(guān)鍵字原理內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Java實(shí)現(xiàn)調(diào)用外部程序的示例代碼
本文主要介紹了Java實(shí)現(xiàn)調(diào)用外部程序的示例代碼,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2023-05-05idea?intellij快速修復(fù)if語(yǔ)句缺少大括號(hào)的問(wèn)題
這篇文章主要介紹了idea?intellij快速修復(fù)if語(yǔ)句缺少大括號(hào)的問(wèn)題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-04-04Java 數(shù)組元素倒序的三種方式(小結(jié))
這篇文章主要介紹了Java 數(shù)組元素倒序的三種方式(小結(jié)),文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2019-09-09springboot集成WebSockets廣播消息(推薦)
這篇文章主要介紹了springboot-集成WebSockets廣播消息,本文給大家介紹的非常詳細(xì),具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2019-12-12Spring和IDEA不推薦使用@Autowired?注解原因解析
這篇文章主要為大家介紹了Spring和IDEA不推薦使用@Autowired?注解原因解析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-07-07