深入理解Java中的volatile關(guān)鍵字(總結(jié)篇)
基本概念
--------------------------------------------------------------------------------
先補(bǔ)充一下概念:Java 內(nèi)存模型中的可見(jiàn)性、原子性和有序性。
可見(jiàn)性:
可見(jiàn)性是一種復(fù)雜的屬性,因?yàn)榭梢?jiàn)性中的錯(cuò)誤總是會(huì)違背我們的直覺(jué)。通常,我們無(wú)法確保執(zhí)行讀操作的線程能適時(shí)地看到其他線程寫(xiě)入的值,有時(shí)甚至是根本不可能的事情。為了確保多個(gè)線程之間對(duì)內(nèi)存寫(xiě)入操作的可見(jiàn)性,必須使用同步機(jī)制。
可見(jiàn)性,是指線程之間的可見(jiàn)性,一個(gè)線程修改的狀態(tài)對(duì)另一個(gè)線程是可見(jiàn)的。也就是一個(gè)線程修改的結(jié)果。另一個(gè)線程馬上就能看到。比如:用volatile修飾的變量,就會(huì)具有可見(jiàn)性。volatile修飾的變量不允許線程內(nèi)部緩存和重排序,即直接修改內(nèi)存。所以對(duì)其他線程是可見(jiàn)的。但是這里需要注意一個(gè)問(wèn)題,volatile只能讓被他修飾內(nèi)容具有可見(jiàn)性,但不能保證它具有原子性。比如 volatile int a = 0;之后有一個(gè)操作 a++;這個(gè)變量a具有可見(jiàn)性,但是a++ 依然是一個(gè)非原子操作,也就是這個(gè)操作同樣存在線程安全問(wèn)題。
在 Java 中 volatile、synchronized 和 final 實(shí)現(xiàn)可見(jiàn)性。
原子性:
原子是世界上的最小單位,具有不可分割性。比如 a=0;(a非long和double類型) 這個(gè)操作是不可分割的,那么我們說(shuō)這個(gè)操作時(shí)原子操作。再比如:a++; 這個(gè)操作實(shí)際是a = a + 1;是可分割的,所以他不是一個(gè)原子操作。非原子操作都會(huì)存在線程安全問(wèn)題,需要我們使用同步技術(shù)(sychronized)來(lái)讓它變成一個(gè)原子操作。一個(gè)操作是原子操作,那么我們稱它具有原子性。java的concurrent包下提供了一些原子類,我們可以通過(guò)閱讀API來(lái)了解這些原子類的用法。比如:AtomicInteger、AtomicLong、AtomicReference等。
在 Java 中 synchronized 和在 lock、unlock 中操作保證原子性。
有序性:
Java 語(yǔ)言提供了 volatile 和 synchronized 兩個(gè)關(guān)鍵字來(lái)保證線程之間操作的有序性,volatile 是因?yàn)槠浔旧戆敖怪噶钪嘏判颉钡恼Z(yǔ)義,synchronized 是由“一個(gè)變量在同一個(gè)時(shí)刻只允許一條線程對(duì)其進(jìn)行 lock 操作”這條規(guī)則獲得的,此規(guī)則決定了持有同一個(gè)對(duì)象鎖的兩個(gè)同步塊只能串行執(zhí)行。
正題
在再有人問(wèn)你Java內(nèi)存模型是什么,就把這篇文章發(fā)給他中我們?cè)?jīng)介紹過(guò),Java語(yǔ)言為了解決并發(fā)編程中存在的原子性、可見(jiàn)性和有序性問(wèn)題,提供了一系列和并發(fā)處理相關(guān)的關(guān)鍵字,比如synchronized、volatile、final、concurren包等。在前一篇文章中,我們也介紹了synchronized的用法及原理。本文,來(lái)分析一下另外一個(gè)關(guān)鍵字——volatile。
本文就圍繞volatile展開(kāi),主要介紹volatile的用法、volatile的原理,以及volatile是如何提供可見(jiàn)性和有序性保障的等。
volatile這個(gè)關(guān)鍵字,不僅僅在Java語(yǔ)言中有,在很多語(yǔ)言中都有的,而且其用法和語(yǔ)義也都是不盡相同的。尤其在C語(yǔ)言、C++以及Java中,都有volatile關(guān)鍵字。都可以用來(lái)聲明變量或者對(duì)象。下面簡(jiǎn)單來(lái)介紹一下Java語(yǔ)言中的volatile關(guān)鍵字。
volatile的用法
volatile通常被比喻成"輕量級(jí)的synchronized",也是Java并發(fā)編程中比較重要的一個(gè)關(guān)鍵字。和synchronized不同,volatile是一個(gè)變量修飾符,只能用來(lái)修飾變量。無(wú)法修飾方法及代碼塊等。
volatile的用法比較簡(jiǎn)單,只需要在聲明一個(gè)可能被多線程同時(shí)訪問(wèn)的變量時(shí),使用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; } }
如以上代碼,是一個(gè)比較典型的使用雙重鎖校驗(yàn)的形式實(shí)現(xiàn)單例的,其中使用volatile關(guān)鍵字修飾可能被多個(gè)線程同時(shí)訪問(wèn)到的singleton。
volatile的原理
在再有人問(wèn)你Java內(nèi)存模型是什么,就把這篇文章發(fā)給他中我們?cè)?jīng)介紹過(guò),為了提高處理器的執(zhí)行速度,在處理器和內(nèi)存之間增加了多級(jí)緩存來(lái)提升。但是由于引入了多級(jí)緩存,就存在緩存數(shù)據(jù)不一致問(wèn)題。
但是,對(duì)于volatile變量,當(dāng)對(duì)volatile變量進(jìn)行寫(xiě)操作的時(shí)候,JVM會(huì)向處理器發(fā)送一條lock前綴的指令,將這個(gè)緩存中的變量回寫(xiě)到系統(tǒng)主存中。
但是就算寫(xiě)回到內(nèi)存,如果其他處理器緩存的值還是舊的,再執(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è)緩存中是可見(jiàn)的。
volatile與可見(jiàn)性
可見(jiàn)性是指當(dāng)多個(gè)線程訪問(wèn)同一個(gè)變量時(shí),一個(gè)線程修改了這個(gè)變量的值,其他線程能夠立即看得到修改的值。
我們?cè)谠儆腥藛?wèn)你Java內(nèi)存模型是什么,就把這篇文章發(fā)給他中分析過(guò):Java內(nèi)存模型規(guī)定了所有的變量都存儲(chǔ)在主內(nèi)存中,每條線程還有自己的工作內(nèi)存,線程的工作內(nèi)存中保存了該線程中是用到的變量的主內(nèi)存副本拷貝,線程對(duì)變量的所有操作都必須在工作內(nèi)存中進(jìn)行,而不能直接讀寫(xiě)主內(nèi)存。不同的線程之間也無(wú)法直接訪問(wèn)對(duì)方工作內(nèi)存中的變量,線程間變量的傳遞均需要自己的工作內(nèi)存和主存之間進(jìn)行數(shù)據(jù)同步進(jìn)行。所以,就可能出現(xiàn)線程1改了某個(gè)變量的值,但是線程2不可見(jiàn)的情況。
前面的關(guān)于volatile的原理中介紹過(guò)了,Java中的volatile關(guān)鍵字提供了一個(gè)功能,那就是被其修飾的變量在被修改后可以立即同步到主內(nèi)存,被其修飾的變量在每次是用之前都從主內(nèi)存刷新。因此,可以使用volatile來(lái)保證多線程操作時(shí)變量的可見(jiàn)性。
volatile與有序性
有序性即程序執(zhí)行的順序按照代碼的先后順序執(zhí)行。
我們?cè)谠儆腥藛?wèn)你Java內(nèi)存模型是什么,就把這篇文章發(fā)給他中分析過(guò):除了引入了時(shí)間片以外,由于處理器優(yōu)化和指令重排等,CPU還可能對(duì)輸入代碼進(jìn)行亂序執(zhí)行,比如load->add->save 有可能被優(yōu)化成load->save->add 。這就是可能存在有序性問(wèn)題。
而volatile除了可以保證數(shù)據(jù)的可見(jiàn)性之外,還有一個(gè)強(qiáng)大的功能,那就是他可以禁止指令重排優(yōu)化等。
普通的變量?jī)H僅會(huì)保證在該方法的執(zhí)行過(guò)程中所依賴的賦值結(jié)果的地方都能獲得正確的結(jié)果,而不能保證變量的賦值操作的順序與程序代碼中的執(zhí)行順序一致。
volatile可以禁止指令重排,這就保證了代碼的程序會(huì)嚴(yán)格按照代碼的先后順序執(zhí)行。這就保證了有序性。被volatile修飾的變量的操作,會(huì)嚴(yán)格按照代碼順序執(zhí)行,load->add->save 的執(zhí)行順序就是:load、add、save。
volatile與原子性
原子性是指一個(gè)操作是不可中斷的,要全部執(zhí)行完成,要不就都不執(zhí)行。
我們?cè)贘ava的并發(fā)編程中的多線程問(wèn)題到底是怎么回事兒?中分析過(guò):線程是CPU調(diào)度的基本單位。CPU有時(shí)間片的概念,會(huì)根據(jù)不同的調(diào)度算法進(jìn)行線程調(diào)度。當(dāng)一個(gè)線程獲得時(shí)間片之后開(kāi)始執(zhí)行,在時(shí)間片耗盡之后,就會(huì)失去CPU使用權(quán)。所以在多線程場(chǎng)景下,由于時(shí)間片在線程間輪換,就會(huì)發(fā)生原子性問(wèn)題。
在上一篇文章中,我們介紹synchronized的時(shí)候,提到過(guò),為了保證原子性,需要通過(guò)字節(jié)碼指令monitorenter和monitorexit,但是volatile和這兩個(gè)指令之間是沒(méi)有任何關(guān)系的。
所以,volatile是不能保證原子性的。
在以下兩個(gè)場(chǎng)景中可以使用volatile來(lái)代替synchronized:
1、運(yùn)算結(jié)果并不依賴變量的當(dāng)前值,或者能夠確保只有單一的線程會(huì)修改變量的值。
2、變量不需要與其他狀態(tài)變量共同參與不變約束。
除以上場(chǎng)景外,都需要使用其他方式來(lái)保證原子性,如synchronized或者concurrent包。
我們來(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次i++操作。正常情況下,程序的輸出結(jié)果應(yīng)該是10000,但是,多次執(zhí)行的結(jié)果都小于10000。這其實(shí)就是volatile無(wú)法滿足原子性的原因。
為什么會(huì)出現(xiàn)這種情況呢,那就是因?yàn)殡m然volatile可以保證inc在多個(gè)線程之間的可見(jiàn)性。但是無(wú)法inc++的原子性。
總結(jié)與思考
我們介紹過(guò)了volatile關(guān)鍵字和synchronized關(guān)鍵字?,F(xiàn)在我們知道,synchronized可以保證原子性、有序性和可見(jiàn)性。而volatile卻只能保證有序性和可見(jiàn)性。
那么,我們?cè)賮?lái)看一下雙重校驗(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; } }
總結(jié)
以上所述是小編給大家介紹的Java中的volatile關(guān)鍵字,希望對(duì)大家有所幫助,如果大家有任何疑問(wèn)請(qǐng)給我留言,小編會(huì)及時(shí)回復(fù)大家的。在此也非常感謝大家對(duì)腳本之家網(wǎng)站的支持!
相關(guān)文章
spring boot+redis 監(jiān)聽(tīng)過(guò)期Key的操作方法
這篇文章主要介紹了spring boot+redis 監(jiān)聽(tīng)過(guò)期Key,本文通過(guò)示例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2020-08-08java編程無(wú)向圖結(jié)構(gòu)的存儲(chǔ)及DFS操作代碼詳解
這篇文章主要介紹了java編程無(wú)向圖結(jié)構(gòu)的存儲(chǔ)及DFS操作代碼詳解,具有一定借鑒價(jià)值,需要的朋友可以了解下。2017-12-12Java躲不過(guò)設(shè)計(jì)模式的坑之代理模式詳解
設(shè)計(jì)模式看來(lái)更像是一種設(shè)計(jì)思維或設(shè)計(jì)思想,為你的項(xiàng)目工程提供方向,讓你的項(xiàng)目工程更加健壯、靈活,延續(xù)生命力。本文即將分享的是設(shè)計(jì)模式的其中一種:代理模式,感興趣的可以了解一下2022-09-09idea中java及java web項(xiàng)目的常見(jiàn)問(wèn)題及解決
在IDEA中處理亂碼問(wèn)題主要涉及四個(gè)方面:文件編碼設(shè)置為UTF-8、編輯器默認(rèn)編碼調(diào)整、Tomcat運(yùn)行配置編碼設(shè)置以及解決cmd中的亂碼,此外,詳細(xì)介紹了在IDEA中創(chuàng)建Web項(xiàng)目的步驟,包括新建Java工程、添加Web框架支持、添加Tomcat依賴庫(kù)2024-09-09Java selenium處理極驗(yàn)滑動(dòng)驗(yàn)證碼示例
本篇文章主要介紹了Java selenium處理極驗(yàn)滑動(dòng)驗(yàn)證碼示例,小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2017-10-10Spring Boot 2 整合 QuartJob 實(shí)現(xiàn)定時(shí)器實(shí)時(shí)管理功能
Quartz是一個(gè)完全由java編寫(xiě)的開(kāi)源作業(yè)調(diào)度框架,形式簡(jiǎn)易,功能強(qiáng)大。接下來(lái)通過(guò)本文給大家分享Spring Boot 2 整合 QuartJob 實(shí)現(xiàn)定時(shí)器實(shí)時(shí)管理功能,感興趣的朋友一起看看吧2019-11-11Java正則表達(dá)式_動(dòng)力節(jié)點(diǎn)Java學(xué)院整理
什么是正則表達(dá)式,正則表達(dá)式的作用是什么?這篇文章主要為大家詳細(xì)介紹了Java正則表達(dá)式的相關(guān)資料,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-05-05