Java中的volatile實(shí)現(xiàn)機(jī)制詳細(xì)解析
前言
在任何編程語(yǔ)言中,多線(xiàn)程操作同一個(gè)數(shù)據(jù)都會(huì)帶來(lái)數(shù)據(jù)不一致的問(wèn)題,這是由于在多線(xiàn)程的情況下CPU分配時(shí)間片并不是按照線(xiàn)程創(chuàng)建順序去分配的,具有一定的隨機(jī)性。
一個(gè)任務(wù)被首先創(chuàng)建出來(lái),并不意味著這個(gè)特定的任務(wù)一定會(huì)首先執(zhí)行,為了解決并發(fā)狀態(tài)下數(shù)據(jù)不一致的問(wèn)題,就有了Lock、synchronized、volatile等等一系列的解決方法。
線(xiàn)程對(duì)資源的感知
我們現(xiàn)在有一個(gè)游戲的充錢(qián)系統(tǒng),這個(gè)系統(tǒng)只有兩種功能:充錢(qián)、顯示余額。我們希望這個(gè)系統(tǒng)的功能是這樣的,如果玩家一旦充錢(qián),賬戶(hù)變化立刻會(huì)被感知到并輸出出來(lái)。假設(shè)賬戶(hù)里有0元:
public class VolatileTest { final static int MAX = 500; //最多500元作為退出條件 static int deposit = 0; //初始余額 public static void main(String[] args) { //顯示賬戶(hù)余額線(xiàn)程 new Thread(() -> { int calculate = deposit; while (calculate < MAX) { if(calculate!=deposit){ //當(dāng)發(fā)現(xiàn)本地變量和全局變量不一致時(shí)輸出 System.out.println("當(dāng)前余額" + deposit); calculate = deposit; } } }).start(); //充錢(qián)線(xiàn)程,每次充錢(qián)100元 new Thread(() -> { int calculate = deposit; while (calculate < MAX) { calculate += 100; //改變金額 System.out.println("充錢(qián)100元,當(dāng)前總額" + calculate); deposit = calculate; //回寫(xiě)給全局變量 try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } } }).start(); } }
運(yùn)行程序,拿到下面的結(jié)果:
結(jié)果發(fā)現(xiàn),我們的感知線(xiàn)程除了第一次感知到變化外,后續(xù)的充錢(qián)都沒(méi)有感知到。但是線(xiàn)程一直再運(yùn)行并沒(méi)有停下。
我們接著分析代碼邏輯,充錢(qián)線(xiàn)程之所以不再打印了是因?yàn)槌隽顺溴X(qián)最大限制while (calculate < MAX),那么理論上來(lái)說(shuō)deposit現(xiàn)在已經(jīng)是500了。
但是在顯示余額的線(xiàn)程上,并沒(méi)有感知到deposit被修改,仍然認(rèn)為while (calculate < MAX)條件依然成立。
在這個(gè)顯示線(xiàn)程中calculate==deposit==100,所以顯示線(xiàn)程會(huì)一直空轉(zhuǎn),直到把CPU資源消耗完畢崩潰才能停下來(lái),這顯然是一個(gè)非常壞的結(jié)果。
如何修改程序呢,其實(shí)也非常簡(jiǎn)單,只要在初始余額變量上加上volatile關(guān)鍵字即可。
static volatile int deposit = 0; //初始余額,加上關(guān)鍵字
重新運(yùn)行輸出結(jié)果,這次不僅程序運(yùn)行符合預(yù)期,而且程序順利執(zhí)行完畢。每次充錢(qián)的線(xiàn)程執(zhí)行完畢,查詢(xún)余額的線(xiàn)程立刻感知到并且執(zhí)行了相應(yīng)的邏輯,整個(gè)程序邏輯上執(zhí)行順利,結(jié)果符合預(yù)期。
問(wèn)題分析
之所以會(huì)發(fā)生這樣的問(wèn)題,其實(shí)和目前的Java的內(nèi)存模型有關(guān)系。我們知道現(xiàn)在的主機(jī)一般都會(huì)在CPU和主存之間方置緩存,用來(lái)緩沖CPU過(guò)快的執(zhí)行速度與主存相對(duì)較慢的IO速度。其實(shí)Java的線(xiàn)程也有類(lèi)似的結(jié)果,只不過(guò)緩存被本地工作空間代替了,類(lèi)似于下圖。但是要說(shuō)明一點(diǎn),我們可以看作每個(gè)線(xiàn)程有自己的私有空間(local workspace)和共享空間(main memory) ,實(shí)際上Java并沒(méi)有在物理內(nèi)存上這樣劃出來(lái)一塊,這只是Java執(zhí)行中的一個(gè)概念模型。物理上仍然是CPU – Cache – Memory這樣的結(jié)構(gòu)。
在這種結(jié)構(gòu)中,主存中的數(shù)據(jù)每個(gè)線(xiàn)程都可以訪(fǎng)問(wèn),但是本地工作空間只有本地線(xiàn)程可以訪(fǎng)問(wèn)。
而本地工作空間中方置的東西就是本地變量和主存資源副本,因此線(xiàn)程并不能直接修改修改主存的數(shù)據(jù),只能讀取到工作空間中,然后由線(xiàn)程拿到CPU中修改。修改完成后,再?gòu)乃接泄ぷ骺臻g刷新回主存。這樣所有的線(xiàn)程就可以拿到最新的數(shù)據(jù)到自己的工作空間里進(jìn)行操作,這個(gè)邏輯在單線(xiàn)程下自然沒(méi)有問(wèn)題。
但是多線(xiàn)程的時(shí)候,就會(huì)出現(xiàn)數(shù)據(jù)不一致的問(wèn)題,比如Thread 0在私有工作空間中做了deposit變量修改,但是還沒(méi)有刷新到主存中的時(shí)候。
Thread 1就把主存中的deposit變量copy到了自己的工作空間進(jìn)行操作,那么Thread 1用的就是舊的數(shù)據(jù),計(jì)算的結(jié)果也就出現(xiàn)了偏差,后來(lái)再經(jīng)過(guò)各個(gè)線(xiàn)程的互相刷新共享空間的數(shù)據(jù),偏差就會(huì)越來(lái)越大。
volatile的原理
根據(jù)我們寫(xiě)的例子來(lái)看,volatile關(guān)鍵字加上以后,就可以保證當(dāng)某個(gè)線(xiàn)程對(duì)主存中數(shù)據(jù)修改的時(shí)候,其他線(xiàn)程能夠感知到這種修改,因此可以基于最新的版本進(jìn)行后續(xù)的操作。
所以volatile關(guān)鍵字應(yīng)該具有以下用作:
保證數(shù)據(jù)的可見(jiàn)性:
某個(gè)線(xiàn)程對(duì)共享數(shù)據(jù)的修改,其他數(shù)據(jù)能夠立刻感知到,這點(diǎn)是如何做到的呢?首先要先引入一個(gè)知識(shí)點(diǎn):緩存的一致性協(xié)議(MESI)。
這個(gè)協(xié)議會(huì)使得:
讀操作,CPU讀取cache中的數(shù)據(jù)時(shí),不做任何鎖操作。
寫(xiě)操作,當(dāng)CPU將要修改某個(gè)共享變量的時(shí)候,CPU會(huì)發(fā)出信號(hào),通知其他的CPU將該變量在其他中緩存中副本所對(duì)應(yīng)的cache line置為無(wú)效,這也就導(dǎo)致了其他CPU的緩存中該變量失效,只能再次從主存中讀取。
有了這個(gè)前提知識(shí)以后,有些讀者一定想到了:一旦給某個(gè)共享變量加上volatile關(guān)鍵字以后,當(dāng)某個(gè)線(xiàn)程要修改共享變量的時(shí)候,會(huì)通知其他線(xiàn)程,來(lái)把其他線(xiàn)程的私有空間中的該共享變量的副本的cache line置為無(wú)效,使得其他線(xiàn)程再次去主存中讀取最新的值,其對(duì)應(yīng)的硬件原理就是上面所說(shuō)的MESI協(xié)議。通過(guò)這樣的辦法,保證了共享變量在各個(gè)線(xiàn)程中的修改可見(jiàn)性,使得所有的線(xiàn)程對(duì)共享變量的修改具有感知。但是重新讀取并不能可以保證一定可以讀取到最新的數(shù)據(jù),因此volatile還必須要有更多的功能。
保證線(xiàn)程的有序性:
程序在編譯階段和指令優(yōu)化階段會(huì)對(duì)執(zhí)行的指令進(jìn)行重排序,也就是說(shuō)我們寫(xiě)的代碼順序,并不是程序的執(zhí)行順序。這樣做的目的是為了提高CPU的執(zhí)行效率和吞吐量,比如賦值指令的執(zhí)行效率明顯會(huì)遠(yuǎn)遠(yuǎn)高于運(yùn)算指令,那么在重排序階段就會(huì)把賦值指令放在一起,運(yùn)算指令放在一起,以提高總體的效率。這樣做在單線(xiàn)程狀態(tài)下沒(méi)有問(wèn)題,但是在多線(xiàn)程狀態(tài)下,一個(gè)變量的先賦值后運(yùn)算和先運(yùn)算后賦值就可能會(huì)產(chǎn)生很明顯的區(qū)別。但是對(duì)于volatile修飾的變量有這樣一個(gè)規(guī)則來(lái)保證程序執(zhí)行的順序:
- volatile之前的代碼不能調(diào)整到它的后面。
- volatile之后的代碼不能調(diào)整到它的前面。
- volatile修飾的代碼,不可以調(diào)整順序。
最終是如何實(shí)現(xiàn)這個(gè)功能的呢?當(dāng)解析到被volatile修飾的變量的時(shí)候,在匯編代碼上該變量會(huì)有一個(gè)Lock標(biāo)記,表示該變量被鎖住。也就是說(shuō)想某一個(gè)線(xiàn)程使用共享變量的時(shí)候,該變量就會(huì)被鎖住。其他線(xiàn)程由于MESI導(dǎo)致必須去主存哪取新數(shù)據(jù)的時(shí)候,會(huì)因?yàn)檫@里有Lock而阻塞,直到這個(gè)線(xiàn)程釋放該共享變量。分析到最后其實(shí)volatile依然是由鎖構(gòu)建的功能,但是這個(gè)鎖也是一個(gè)輕量級(jí)的鎖。
注意:volatile即使具有上述這些作用,但是并不具有原子性。
volatile的應(yīng)用
說(shuō)了半天volatile關(guān)鍵字的原理,這里列舉幾個(gè)常用的場(chǎng)景。
Flag標(biāo)志:作為控制某個(gè)功能或者分支的flag標(biāo)志使用。
public class VolatileTest implements Runnable{ private volatile boolean flag=false; @Override public void run() { if (flag){ //...code }else{ //...code } } public void close(){ flag=false; } }
雙重檢查鎖定:Double Checked Locking(DCL),一般用在單例模式上。
public class VolatileTest2 { private volatile static VolatileTest2 vt; public static VolatileTest2 getInstance(){ if(Objects.isNull(vt)){ vt=new VolatileTest2(); } return vt; } }
程序的執(zhí)行順序必須保證:如果有場(chǎng)景中必須要求某些變量在程序的限定位置出現(xiàn),而且不能隨意變更執(zhí)行順序,那么可以對(duì)這些變量加上volatile去保證,這些代碼的執(zhí)行順序是完全按照代碼所寫(xiě)的順序執(zhí)行的。
volatile與synchronized的區(qū)別
之前的博客已經(jīng)對(duì)synchronized做了講解,我們知道synchronized是加鎖用的,那么volatile和synchronized有什么區(qū)別呢?我們用一個(gè)表格做個(gè)對(duì)比。
區(qū)別 | volatile | synchronized |
語(yǔ)法上 | 只能修飾變量 | 只能修飾方法和語(yǔ)句塊 |
原子性 | 不能保證原子性 | 可以保證原子性 |
可見(jiàn)性 | 通過(guò)對(duì)變量加lock,使用緩存的一致性協(xié)議保證可見(jiàn)性 | 使用對(duì)象監(jiān)視器monitor保證可見(jiàn)性,monitorenter,monitorexit,ACC_SYNCHRONIZED |
有序性 | 可以保證有序性 | 可以保證有序性,但是加鎖部分變?yōu)閱尉€(xiàn)程執(zhí)行 |
阻塞 | 輕量級(jí)鎖不會(huì)阻塞 | 偏向鎖,可能會(huì)引發(fā)阻塞; 重量級(jí)鎖,會(huì)引起阻塞 |
總結(jié)
本文的主要內(nèi)容就在于要理解volatile的緩存的一致性協(xié)議導(dǎo)致的共享變量可見(jiàn)性,以及volatile在解析成為匯編語(yǔ)言的時(shí)候?qū)ψ兞考渔i兩塊理論內(nèi)容。
到此volatile關(guān)鍵字的基本內(nèi)容告一段落,謝謝大家。
到此這篇關(guān)于Java中的volatile實(shí)現(xiàn)機(jī)制詳細(xì)解析的文章就介紹到這了,更多相關(guān)volatile實(shí)現(xiàn)機(jī)制內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
SpringBoot大學(xué)心理服務(wù)系統(tǒng)實(shí)現(xiàn)流程分步講解
本系統(tǒng)主要論述了如何使用JAVA語(yǔ)言開(kāi)發(fā)一個(gè)大學(xué)生心理服務(wù)系統(tǒng) ,本系統(tǒng)將嚴(yán)格按照軟件開(kāi)發(fā)流程進(jìn)行各個(gè)階段的工作,采用B/S架構(gòu),面向?qū)ο缶幊趟枷脒M(jìn)行項(xiàng)目開(kāi)發(fā)2022-09-09SpringBoot實(shí)現(xiàn)對(duì)超大文件進(jìn)行異步壓縮下載的使用示例
在Web應(yīng)用中,文件下載功能是一個(gè)常見(jiàn)的需求,本文介紹了SpringBoot實(shí)現(xiàn)對(duì)超大文件進(jìn)行異步壓縮下載的使用示例,具有一定的參考價(jià)值,感興趣的可以了解一下,2023-09-09Netty分布式ByteBuf中PooledByteBufAllocator剖析
這篇文章主要為大家介紹了Netty分布式ByteBuf剖析PooledByteBufAllocator簡(jiǎn)述,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-03-03MyBatisPlus 一對(duì)多、多對(duì)一、多對(duì)多的完美解決方案
這篇文章主要介紹了MyBatisPlus 一對(duì)多、多對(duì)一、多對(duì)多的完美解決方案,本文通過(guò)圖文并茂的形式給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2020-11-11Mybatis分頁(yè)插件PageHelper的配置和簡(jiǎn)單使用方法(推薦)
在使用Java Spring開(kāi)發(fā)的時(shí)候,Mybatis算是對(duì)數(shù)據(jù)庫(kù)操作的利器了。這篇文章主要介紹了Mybatis分頁(yè)插件PageHelper的配置和使用方法,需要的朋友可以參考下2017-12-12Java內(nèi)存劃分:運(yùn)行時(shí)數(shù)據(jù)區(qū)域
聽(tīng)說(shuō)Java運(yùn)行時(shí)環(huán)境的內(nèi)存劃分是挺進(jìn)BAT的必經(jīng)之路,這篇文章主要給大家介紹了關(guān)于Java運(yùn)行時(shí)數(shù)據(jù)區(qū)域(內(nèi)存劃分)的相關(guān)資料,需要的朋友可以參考下2021-07-07Java使用CountDownLatch實(shí)現(xiàn)網(wǎng)絡(luò)同步請(qǐng)求的示例代碼
CountDownLatch 是一個(gè)同步工具類(lèi),用來(lái)協(xié)調(diào)多個(gè)線(xiàn)程之間的同步,它能夠使一個(gè)線(xiàn)程在等待另外一些線(xiàn)程完成各自工作之后,再繼續(xù)執(zhí)行。被將利用CountDownLatch實(shí)現(xiàn)網(wǎng)絡(luò)同步請(qǐng)求,異步同時(shí)獲取商品信息組裝,感興趣的可以了解一下2023-01-01idea左側(cè)的commit框設(shè)置顯示出來(lái)方式
在IDEA中顯示左側(cè)的commit框,首先通過(guò)File-Settings-Version Control-Commit進(jìn)行設(shè)置,然后勾選Use non-modal commit interface完成2025-01-01