詳細(xì)分析java并發(fā)之volatile關(guān)鍵字
Java面試中經(jīng)常會(huì)涉及關(guān)于volatile的問(wèn)題。本文梳理下volatile關(guān)鍵知識(shí)點(diǎn)。
volatile字意為“易失性”,在Java中用做修飾對(duì)象變量。它不是Java特有,在C,C++,C#等編程語(yǔ)言也存在,只是在其它編程語(yǔ)言中使用有所差異,但總體語(yǔ)義一致。比如使用volatile 能阻止編譯器對(duì)變量的讀寫(xiě)優(yōu)化。簡(jiǎn)單說(shuō),如果一個(gè)變量被修飾為volatile,相當(dāng)于告訴系統(tǒng)說(shuō)我容易變化,編譯器你不要隨便優(yōu)化(重排序,緩存)我。
Happens-before
規(guī)范上,Java內(nèi)存模型遵行happens-before。
volatile變量在多線程中,寫(xiě)線程和讀線程具有happens-before關(guān)系。也就是寫(xiě)值的線程要在讀取線程之前,并且讀線程能完全看見(jiàn)寫(xiě)線程的相關(guān)變量。
happens-before:如果兩個(gè)有兩個(gè)動(dòng)作AB,A發(fā)生在B之前,那么A的順序應(yīng)該在B前面并且A的操作對(duì)B完全可見(jiàn)。
happens-before 具有傳遞性,如果A發(fā)生在B之前,而B(niǎo)發(fā)生在C之前,那么A發(fā)生在C之前。
如何保證可見(jiàn)性
多線程環(huán)境下counter變量的更新過(guò)程。線程1先從主存拷貝副本到CPU緩存,然后CPU執(zhí)行counter=7,修改完后寫(xiě)入CPU緩存,等待時(shí)機(jī)同步到主存。在線程1同步主存前,線程2讀到counter值依然為0。此時(shí)已經(jīng)發(fā)生內(nèi)存一致性錯(cuò)誤(對(duì)于相同的共享數(shù)據(jù),多線程讀到視圖不一致)。因?yàn)榫€程2看不見(jiàn)線程1操作結(jié)果,也將這個(gè)問(wèn)題稱為可見(jiàn)性問(wèn)題。
public class SharedObject { public int counter = 0; }
因?yàn)槎嗔司彺鎯?yōu)化導(dǎo)致,導(dǎo)致可見(jiàn)性問(wèn)題。所以volatile通過(guò)消除緩存(描述可能不太準(zhǔn)確)來(lái)避免。例如當(dāng)使用volatile修飾變量后,操作該變量讀寫(xiě)直接與主存交互,跳過(guò)緩存層,保證其它讀線程每次獲取的都是最新值。
public volatile int counter = 0;
volatile 不單只消除修飾的變量的緩存。事實(shí)上與之相關(guān)的變量在讀寫(xiě)時(shí)也會(huì)消除緩存,如同使用了volatile一樣。
如下 years,months,days 三個(gè)變量中只有days是volatile,但是對(duì)years,months讀寫(xiě)操作也和days時(shí)也會(huì)跳過(guò)緩存,其它線程每次讀到的都是最新值。
public class MyClass { private int years; private int months private volatile int days; public int totalDays() { int total = this.days; total += months * 30; total += years * 365; return total; } public void update(int years, int months, int days){ this.years = years; this.months = months; this.days = days; } }
這是為什么?我們分析一下。
一個(gè)寫(xiě)線程調(diào)用 update,讀線程調(diào)用totalDays。單線程中,對(duì)于update方法,wa與wb存在happens-before關(guān)系, wa在 wb 之前執(zhí)行并對(duì)wb可見(jiàn)。
多線程中rc與wb存在happens-before關(guān)系,wb在rc之前執(zhí)行并對(duì)rc可見(jiàn)。根據(jù) happens-before傳遞性,wa需要在rc前先執(zhí)行并對(duì)rc可見(jiàn)。
因?yàn)閣b是volatile變量,所以rc獲取的years,months也是最新值。
我們知道出于性能原因,JVM和CPU會(huì)對(duì)程序中的指令進(jìn)行重新排序。如果update方法里面wa和wb順序被重排,那它們的happens-before關(guān)系將不在成立。
為了避免這個(gè)問(wèn)題,volatile對(duì)重排序做了保證 對(duì)于發(fā)生在volatile變量操作前的其他變量的操作不能重新排序。
由此我們得到volatile通過(guò)消除緩存和防止重排保證線程的可見(jiàn)性。
volatile保證線程安全?
討論線程安全,大家都會(huì)提及原子性,順序性,可見(jiàn)性。volatile側(cè)重于保證可見(jiàn)性,也就是當(dāng)寫(xiě)的線程更新后,讀線程總能獲得最新值。在只有一個(gè)線程寫(xiě),多個(gè)線程讀的場(chǎng)景下,volatile能滿足線程安全??扇绻鄠€(gè)線程同時(shí)寫(xiě)入volatile變量時(shí),則需要引入同步語(yǔ)義才能保證線程安全。
模擬10個(gè)線程同時(shí)寫(xiě)入volatile變量,一個(gè)線程讀counter,執(zhí)行完后正確結(jié)果應(yīng)該是counter=10。
public static class WriterTask implements Runnable { private final ShareObject share; private final CountDownLatch countDownLatch; public WriterTask(ShareObject share, CountDownLatch countDownLatch) { this.share = share; this.countDownLatch = countDownLatch; } @Override public void run() { countDownLatch.countDown(); share.increase(); } } public class ShareObject { private volatile int counter; public void increase() { this.counter++; } }
執(zhí)行結(jié)果出現(xiàn)counter=5或6 錯(cuò)誤結(jié)果。
通過(guò) synchronized,Lock或AtomicInteger 原子變量保證了結(jié)果的正確。
完整demo https://gist.github.com/onlythinking/ba7ca7aa5faf00a58f4cedae474fa6f6
volatile性能
volatile變量帶來(lái)可見(jiàn)性的保證,訪問(wèn)volatile變量還防止了指令重排序。不過(guò)這一切是以犧牲優(yōu)化(消除緩存,直接操作主存開(kāi)銷增加)為代價(jià),所以不應(yīng)該濫用volatile,僅在確實(shí)需要增強(qiáng)變量可見(jiàn)性的時(shí)候使用。
總結(jié)
本文記錄了volatile變量通過(guò)消除緩存,防止指令重排序來(lái)保證線程可見(jiàn)性,并且在多線程寫(xiě)入的變量的場(chǎng)景下,不保證線程安全。
歡迎大家留言交流,一起學(xué)習(xí)分享?。?!
以上就是詳細(xì)分析java并發(fā)之volatile關(guān)鍵字的詳細(xì)內(nèi)容,更多關(guān)于JAVA volatile關(guān)鍵字的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Java如何將任意類型的Object對(duì)象轉(zhuǎn)換為相應(yīng)的實(shí)體對(duì)象
這篇文章主要介紹了Java如何將任意類型的Object對(duì)象轉(zhuǎn)換為相應(yīng)的實(shí)體對(duì)象問(wèn)題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2024-01-01Java并發(fā)編程之ConcurrentLinkedQueue解讀
這篇文章主要介紹了Java并發(fā)編程之ConcurrentLinkedQueue解讀,非阻塞的實(shí)現(xiàn)方式則可以使用循環(huán)CAS的方式來(lái)實(shí)現(xiàn),而ConcurrentLinkedQueue就是juc包中自帶的經(jīng)典非堵塞方式實(shí)現(xiàn)的工具類,需要的朋友可以參考下2023-12-12基于mybatis?plus實(shí)現(xiàn)數(shù)據(jù)源動(dòng)態(tài)添加、刪除、切換,自定義數(shù)據(jù)源的示例代碼
這篇文章主要介紹了基于mybatis?plus實(shí)現(xiàn)數(shù)據(jù)源動(dòng)態(tài)添加、刪除、切換,自定義數(shù)據(jù)源,本文通過(guò)實(shí)例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2022-03-03淺談一下maven優(yōu)缺點(diǎn)及使用和特點(diǎn)
這篇文章主要介紹了淺談一下maven優(yōu)缺點(diǎn)及使用和特點(diǎn),一個(gè)項(xiàng)目管理工具軟件,那么maven項(xiàng)目有什么優(yōu)缺點(diǎn)呢,讓我們一起來(lái)看看吧2023-03-03SpringBoot返回多種格式的數(shù)據(jù)的實(shí)現(xiàn)示例
本文主要介紹了SpringBoot返回多種格式的數(shù)據(jù)的實(shí)現(xiàn)示例,主要包括了FastJson,xml,pdf,excel,資源流,具有一定的參考價(jià)值,感興趣的可以了解一下2021-10-10