日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當(dāng)前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

面试造飞机系列:volatile面试的连环追击,你还好吗?

發(fā)布時間:2025/3/20 编程问答 41 豆豆
生活随笔 收集整理的這篇文章主要介紹了 面试造飞机系列:volatile面试的连环追击,你还好吗? 小編覺得挺不錯的,現(xiàn)在分享給大家,幫大家做個參考.

?點擊上方?好好學(xué)java?,選擇?星標(biāo)?公眾號

重磅資訊、干貨,第一時間送達(dá) 今日推薦:為什么程序員都不喜歡使用switch,而是大量的 if……else if ?個人原創(chuàng)+1博客:點擊前往,查看更多

本文腦圖


volatile是java中熱門關(guān)鍵字,也是面試中的高頻問點,今天就來深入的從各種volatile面試題中剖析它的底層原理實現(xiàn),并通過簡單的代碼去證明。

在深入volatile之前,我們先從原理入手,然后層層深入,逐步剖析它的底層原理,使用過volatile關(guān)鍵字的程序員都知道,在多線程并發(fā)場景中volitile能夠保障共享變量的可見性

那么問題來了,什么是可見性呢?volatile是怎么保障共享變量的可見性的呢?

附上我歷時三個月總結(jié)的?Java 面試 + Java 后端技術(shù)學(xué)習(xí)指南,這是本人這幾年及春招的總結(jié),目前,已經(jīng)拿到了大廠offer,拿去不謝!

下載方式

1.?首先掃描下方二維碼

2.?后臺回復(fù)「Java面試」即可獲取

在說可見性之前,我們先來了解在多線程的條件下,線程與線程之間是怎么通信的,我們先來看看一張圖:


在Java線程中每次的讀取寫入不會直接操作主內(nèi)存,因為cpu的速度遠(yuǎn)快于主內(nèi)存的速度,若是直接操作主內(nèi)存,大大限制了cpu的性能,對性能有很大的影響,所以每條線程都有各自的工作內(nèi)存

這里的工作內(nèi)存類似于緩存,并非實際存在的,因為緩存的讀取和寫入的速度遠(yuǎn)大于主內(nèi)存,這樣就大大提高了cpu與數(shù)據(jù)交互的性能

所有的共享變量都是直接存儲于主內(nèi)存中,工作內(nèi)存保存線程在使用主內(nèi)存共享變量的副本,當(dāng)操作完工作內(nèi)存的變量,會寫入主內(nèi)存,完成對共享變量的讀取和寫入。

在單線程時代,不存在數(shù)據(jù)一致性的的問題,線程都是排隊的順序執(zhí)行,前面的線程執(zhí)行完才會到后面的線程執(zhí)行。



隨著計算機(jī)的發(fā)展,到了多核多線程的時代,緩存的出現(xiàn)雖然提升了cpu的執(zhí)行效率,但是卻出現(xiàn)了緩存一致性的問題,為了解決數(shù)據(jù)的一致性問題,提出兩種解決方案:
  • 總線上加Lock#鎖:該方法簡單粗暴,在總線上加鎖,其它cpu的線程只能排隊等候,效率低下。

  • 緩存一致性協(xié)議:該方案是JMM中提出的解決方案,通過對變量地址加鎖,減小鎖的粒度,執(zhí)行變得更加高效。

  • 為了提高程序的執(zhí)行效率,設(shè)計者們提出了底層對編譯器和執(zhí)行器(處理器)的優(yōu)化方案,分別是編譯器處理器重排序

    那么什么是編譯器重排序和處理器啊重排序呢?

    編譯器重排序就是在不改變單線程的語義的前提下,可以重新排列語句的執(zhí)行順序。

    處理器排序是在機(jī)器指令的層面,假如不存在數(shù)據(jù)依賴,處理器可以改變機(jī)器指令的執(zhí)行順序,為了提高程序的執(zhí)行效率,在多線程中假如兩行的代碼存在數(shù)據(jù)依賴,將會被禁止重排序。

    不管是編譯器重排序處理器的重排序,前提條件都不能改變單線程語義的前提下進(jìn)行重排序,說白了就是最后的執(zhí)行結(jié)果要準(zhǔn)確無誤

    學(xué)過大學(xué)的計算機(jī)基礎(chǔ)課都知道,我們的程序用高級語言寫完后是不能被各大平臺的機(jī)器所執(zhí)行的,需要執(zhí)行編譯,然后將編譯后的字節(jié)碼文件處理成機(jī)器指令,才能被計算機(jī)執(zhí)行。

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

    在這里插入圖片描述
    前面說到了數(shù)據(jù)依賴的特性,什么是數(shù)據(jù)依賴呢?

    數(shù)據(jù)依賴就是假設(shè)一句代碼中對一個變量a++自增,然后后一句代碼b=a將a的值賦值給b,便表示這兩句代碼存在數(shù)據(jù)依賴,兩句代碼執(zhí)行順序不能互換。

    前面提到編譯器和處理器的重排序,在編譯器和處理器進(jìn)行重排序的時候,就會遵守數(shù)據(jù)的依賴性,編譯器和處理器就會禁止存在數(shù)據(jù)依賴的兩個操作進(jìn)行重排序,保證了數(shù)據(jù)的準(zhǔn)確性。

    在JDK5開始,為了保證程序的有序性,便提出了happen-before原則,假如兩個操作符合該原則,那么這兩個操作可以隨意的進(jìn)行重排序,并不會影響結(jié)果的正確性。

    具體happen-before原則有6條,具體原則如下所示:

  • 同一個線程中前面的操作先于后續(xù)的操作(但是這個并不是絕對的,假如在單線程的環(huán)境下,重排序后不會影響結(jié)果的準(zhǔn)確性,是可以進(jìn)行重排序,不按代碼的順序執(zhí)行)。

  • Synchronized 規(guī)則中解鎖操作先于后續(xù)的加鎖操作。

  • volatile 規(guī)則中寫操作先于后續(xù)的讀取操作,保證數(shù)據(jù)的可見性。

  • 一個線程的start()方法先于任何該線程的所有后續(xù)操作。

  • 線程的所有操作先于其他該線程在該線程上調(diào)用join返回成功的操作。

  • 如果操作a先于操作b,操作b先于操作c,那么操作a先于操作c,傳遞性原理。

  • 我們來看重點第三條,也就是我們今天所了解的重點volatile關(guān)鍵字,為了實現(xiàn)volatile內(nèi)存語義,規(guī)定有volatile修飾的共享變量在機(jī)器指令層面會出出現(xiàn)Lock前綴的指令。

    我們來看看一個例子經(jīng)典的例子,具體的代碼如下:

    public?class?TestVolatile?extends?Thread?{private?static?boolean?flag?=?false;public?void?run()?{while?(!flag)?;System.out.println("run方法退出了")}public?static?void?main(String[]?args)?throws?Exception?{new?TestVolatile().start();Thread.sleep(5000);flag?=?true;} }

    看上面的代碼執(zhí)行run方法能執(zhí)行退出嗎?是不能的,因為對于這兩個線程來說,首先new TestVolatile().start()線程拿到flag共享變量的值為false,并存儲在于自己的工作內(nèi)存中。

    第一個線程到while循環(huán)中,就直接進(jìn)入死循環(huán),即使主線程讀取flag的值,然后改變該值為true。

    但是對于第一個線程來說并不知道,flag的值已經(jīng)被修改,在第一個線程的工作內(nèi)存中flag仍然為false。具體的執(zhí)行原理圖如下:


    這樣對于共享變量flag,主線程修改后,對于線程1來說是不可見的,然后我們加上volatile變量修飾該變量,修改代碼如下:?private?static?volatile?boolean?flag?=?false;

    輸出的結(jié)果中,就會輸出run方法退出了,具體的原理假如一個共享變量被Volatile修飾,該指令在多核處理器下會引發(fā)兩件事情。

  • 將當(dāng)前處理器緩存行數(shù)據(jù)寫回主內(nèi)存中。

  • 這個寫入的操作會讓其它處理器中已經(jīng)緩存了該變量的內(nèi)存地址失效,當(dāng)其它處理器需求再次使用該變量時,必須從主內(nèi)存中重新讀取該值。

  • 讓我們具體從idea的輸出的匯編指令中可以看出,我們看到紅色線框里面的那行指令:putstatic flag ,將靜態(tài)變量flag入棧,注意觀察add指令前面有一個lock前綴指令。

    注意:讓idea輸出程序的匯編指令,在啟動程序的時候,可以加上
    -XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly作為啟動參數(shù),就可以查看匯編指令。

    簡單的說被volatile修飾的共享變量,在lock指令后是一個原子操作,該原子操作不會被其它線程的調(diào)度機(jī)制打斷,該原子操作一旦執(zhí)行就會運行到結(jié)束,中間不會切換到任意一個線程。

    當(dāng)使用lock前綴的機(jī)器指令,它會向cpu發(fā)送一個LOCK#信號,這樣能保證在多核多線程的情況下互斥的使用該共享變量的內(nèi)存地址。直到執(zhí)行完畢,該鎖定才會消失。

    volatile的底層就是通過內(nèi)存屏障來實現(xiàn)的,lock前綴指令就相當(dāng)于一個內(nèi)存屏障

    那么什么又是內(nèi)存屏障呢?

    內(nèi)存屏障是一組CPU指令,為了提高程序的運行效率,編譯器和處理器運行對指令進(jìn)行重排序,JMM為了保證程序運行結(jié)果的準(zhǔn)確性,規(guī)定存在數(shù)據(jù)依賴的機(jī)器指令禁止重排序

    通過插入特定類型的內(nèi)存屏障(例如lock前綴指令)來禁止特定類型的編譯器重排序和處理器重排序,插入一條內(nèi)存屏障會告訴編譯器和CPU:不管什么指令都不能和這條Memory Barrier指令重排序。

    所以為了保證每個cpu的數(shù)據(jù)一致性,每一個cpu會通過嗅探總線上傳播的數(shù)據(jù)來檢查自己數(shù)據(jù)的有效性,當(dāng)發(fā)現(xiàn)自己緩存的數(shù)據(jù)的內(nèi)存地址被修改,就會讓自己緩存該數(shù)據(jù)的緩存行失效,重新獲取數(shù)據(jù),保證了數(shù)據(jù)的可見性。

    那么既然volatile可以保證可見性,它可以保證數(shù)據(jù)的原子性嗎?

    什么是原子性呢?原子性就是即不可再分了,不能分為多步操作。在Java中只有對基本類型變量的賦值和讀取才是原子操作。

    如i = 1,但是像j = i或者i++都不是原子操作,因為他們都進(jìn)行了多次原子操作,比如先讀取i的值,再將i的值賦值給j,兩個原子操作加起來就不是原子操作了。

    所以假如一個volatile的integer自增(i++),其實要分成3步:

  • 讀取主內(nèi)存中volatile變量值到工作內(nèi)存;

  • 在工作內(nèi)存中增加變量的值;

  • 把工作內(nèi)存的值寫主內(nèi)存。

  • 假如有兩個線程都要執(zhí)行a變量的自增操作,當(dāng)線程1執(zhí)行a++;語句時,先是讀入a的值為0,此時a線程的執(zhí)行時間被讓出。

    線程2獲得執(zhí)行,線程2會重新從主內(nèi)存中,讀入a的值還是0,然后線程2執(zhí)行+1操作,最后把a(bǔ)=1刷新到主內(nèi)存中;

    線程2執(zhí)行完后,線程1又開始執(zhí)行,但之前已經(jīng)讀取的a的值0,因為前面的讀取原子操作已經(jīng)結(jié)束了,所以它還是在0的基礎(chǔ)上執(zhí)行+1操作,也就是還是等于1,并刷新到主內(nèi)存中。所以最終的結(jié)果是a變量的值為1

    最后,再附上我歷時三個月總結(jié)的?Java 面試 + Java 后端技術(shù)學(xué)習(xí)指南,這是本人這幾年及春招的總結(jié),目前,已經(jīng)拿到了大廠offer,拿去不謝!

    下載方式

    1.?首先掃描下方二維碼

    2.?后臺回復(fù)「Java面試」即可獲取

    總結(jié)

    以上是生活随笔為你收集整理的面试造飞机系列:volatile面试的连环追击,你还好吗?的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網(wǎng)站內(nèi)容還不錯,歡迎將生活随笔推薦給好友。