深度解析volatile—底層實現

      網友投稿 685 2022-05-30

      我們都知道,Java關鍵字Volatile的作用

      1、內存可見性

      2、禁止指令重排序

      可見性是指,在多線程環境,共享變量的操作對于每個線程來說,都是內存可見的,也就是每個線程獲取的volatile變量都是最新值;并且每個線程對volatile變量的修改,都直接刷新到主存。

      下面重點介紹指令重排序。

      為什么要指令重排序?

      為了提高程序執行的性能,編譯器和執行器(處理器)通常會對指令做一些優化(重排序)

      1、編譯器重排序。編譯器在不改變單線程程序語義的前提下,可以重新安排語句的執行順序;

      2、處理器重排序。如果不存在數據依賴性,處理器可以改變語句對應機器指令的執行順序;

      學過《編譯原理》同學應該知道,現代高級編程語言的編譯器,實現都很復雜。

      編譯器基本構造包括:語法分析、詞法分析、語義分析、中間代碼生成、指令優化、目標代碼產生。

      第一階段:編譯器優化,就是發生在編譯階段,就Java而言,就是java源碼編譯生成class字節碼的時候,對編譯生成的中間代碼進行的一次指令優化。Java的編譯器是javac.exe。

      第二階段:執行器(處理器)優化,和不同的處理器硬件廠商的實現有關,也和Java的執行器(java.exe,也稱Java解釋器)有關。執行器優化,是對于機器指令在目標平臺的機器上運行,做的一層優化。

      我們知道,現代高級編程語言,經過編譯后,產生目標代碼,如.java的源文件編譯后生成.class字節碼文件,.cpp源文件經過C++編譯器編譯后生成.o對象文件。

      這些編譯后生成的文件,不能直接在機器上運行,而是需要轉化成特定平臺的機器指令。機器能夠運行的指令,是需要這個平臺、這個機器能正確識別的。

      相同的一份源碼,最終轉化成不同平臺上的機器指令,是不同的。

      這也更容易理解:匯編指令,并不是跨平臺的。Windows下通常使用Intel匯編,而Linux下多用AT&T匯編,它們在語法上存在差異,運行效果也依賴于各自平臺的實現。

      在Java中,為了提高運行效率,javac編譯器,和java解釋器,在2個階段分別對指令進行了優化,也就是重排序。

      Java重排序的前提:在不影響 單線程運行結果的前提下進行重排序。也就是在單線程環境運行,重排序后的結果和重排序之前按代碼順序運行的結果相同。

      深度解析volatile—底層實現

      指令重排序對單線程沒有什么影響,它不會影響程序的運行結果,但是會影響多線程的正確性。

      Java因為指令重排序,優化我們的代碼,讓程序運行更快,也隨之帶來了多線程下,指令執行順序的不可控。既然指令重排序會影響到多線程執行的正確性,那么我們就需要某些情景下禁止重排序。Java提供給我們禁止重排序能力的操作——就是volatile。

      那么JVM的volatile是如何禁止重排序的呢?

      在具體探究之前,我們先看另一個原則happens-before,happen-before原則保證了程序的“有序性”,它規定如果兩個操作的執行順序無法從happens-before原則中推到出來,那么他們就不能保證有序性,可以隨意進行重排序。其定義如下:

      1、同一個線程中的,前面的操作 happen-before 后續的操作。(即單線程內按代碼順序執行。但是,在不影響在單線程環境執行結果的前提下,編譯器和處理器可以進行重排序,這是合法的。換句話說,這一是規則無法保證編譯重排和指令重排)。

      2、監視器上的解鎖操作 happen-before 其后續的加鎖操作。(Synchronized 規則)

      3、對volatile變量的寫操作 happen-before 后續的讀操作。(volatile 規則)

      4、線程的start() 方法 happen-before 該線程所有的后續操作。(線程啟動規則)

      5、線程所有的操作 happen-before 其他線程在該線程上調用 join 返回成功后的操作。

      6、如果 a happen-before b,b happen-before c,則a happen-before c(傳遞性)。

      在JVM中,將Happens-Before的程序順序規則與其他某個順序規則(通常是監視器鎖規則、volatile變量規則)結合起來,從而對某個未被鎖保護的變量的訪問操作進行排序。

      我們著重看第三點volatile規則:對volatile變量的寫操作 happen-before 后續的讀操作。為了實現volatile內存語義,JMM會重排序,其規則如下:

      是否重排序 第二個操作

      第一個操作 普通讀/寫 volatile讀 volatile寫

      普通讀/寫

      volatile讀 NO NO NO

      volatile寫 NO NO

      為了探究volatile底層的實現原理,進行了如下探究。

      通過javap 命令,將字節碼文件反編譯。觀察反編譯的結果,對于volatile修飾的變量,發現反編譯得到的代碼并沒有什么幫助,和不加volatile修飾的變量沒有任何區別。也就是說,字節碼層面volatile變量并沒有什么不同。

      下面通過查看Java的匯編指令,查看Java代碼最真實的運行細節。

      如何查看Java的匯編指令,可以閱讀:https://www.jianshu.com/p/93821b08e774

      通過使用-XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly

      IDEA打印出了源代碼的匯編指令。我們看到紅色線框里面的那行指令:putstatic a ,將靜態變量a入棧,注意觀察add指令前面有一個lock前綴指令。

      加入volatile關鍵字和沒有加入volatile關鍵字時所生成的匯編代碼發現,加入volatile關鍵字時,會多出一個lock前綴指令。我們發現,volatile變量在字節碼級別沒有任何區別,在匯編級別使用了lock指令前綴。

      lock是一個指令前綴,Intel的手冊上對其的解釋是:

      Causes the processor's LOCK# signal to be asserted during execution of the accompanying instruction (turns the instruction into an atomic instruction). In a multiprocessor environment, the LOCK# signal insures that the processor has exclusive use of any shared memory while the signal is asserted.

      簡單理解也就是說,lock后就是一個原子操作。原子操作是指不會被線程調度機制打斷的操作;這種操作一旦開始,就一直運行到結束,中間不會有任何 context switch (切換到另一個線程)。

      當使用 LOCK 指令前綴時,它會使 CPU 宣告一個 LOCK# 信號,這樣就能確保在多處理器系統或多線程競爭的環境下互斥地使用這個內存地址。當指令執行完畢,這個鎖定動作也就會消失。

      是不是感覺有點像Java的synchronized鎖。但volatile底層使用多核處理器實現的lock指令,更底層,消耗代價更小。

      因此有人將Java的synchronized看作重量級的鎖,而volatile看作輕量級的鎖 并不是全無道理。

      lock前綴指令其實就相當于一個內存屏障。內存屏障是一組CPU處理指令,用來實現對內存操作的順序限制。volatile的底層就是通過內存屏障來實現的。

      編譯器和執行器 可以在保證輸出結果一樣的情況下對指令重排序,使性能得到優化。插入一個內存屏障,相當于告訴CPU和編譯器先于這個命令的必須先執行,后于這個命令的必須后執行。正如去西藏途中各個站點的先后順序在你心中都一清二楚。

      內存屏障另一個作用是強制更新一次不同CPU的緩存。例如,一個寫屏障會把這個屏障前寫入的數據刷新到緩存,這樣任何試圖讀取該數據的線程將得到最新值,而不用考慮到底是被哪個cpu核心或者哪個CPU執行的。這正是volatile實現內存可見性的基礎。

      內存屏障細說來有寫屏障、讀屏障、讀寫屏障,而且內存屏障的實現依賴于編譯器和機器兩部分。

      編譯器在編譯過程中可能會對指令重排序,這樣開發者通過顯式地標注告知編譯器,避免編譯器最終生成的代碼行為違背預期,對于 Java 而言,不光生成的 bytecode 需要保存 volatile 的語義,連運行時的 JIT 代碼的行為也要遵守相應的約束;即插入內存屏障后,告訴CPU和編譯器先于這個命令的必須先執行,后于這個命令的必須后執行,從而實現了禁止重排序。

      關于內存屏障的一些具體細節,大佬Martin寫了一篇文章《going into memory barriers》介紹,外網可以看看。

      小結:

      1、Java重排序的前提:在不影響 單線程運行結果的前提下進行重排序。也就是在單線程環境運行,重排序后的結果和重排序之前按代碼順序運行的結果相同。

      2、指令重排序對單線程沒有什么影響,它不會影響程序的運行結果,反而會優化執行性能,但會影響多線程的正確性。

      3、Java因為指令重排序,優化我們的代碼,讓程序運行更快,也隨之帶來了多線程下,指令執行順序的不可控。

      4、volatile的底層是通過lock前綴指令、內存屏障來實現的。

      存檔文章

      查看Java的匯編指令

      終于有人把Java內存模型(JMM)說清楚了

      JVM體系結構-----深入理解內存結構

      從多核硬件架構,看Java內存模型

      版權聲明:本文內容由網絡用戶投稿,版權歸原作者所有,本站不擁有其著作權,亦不承擔相應法律責任。如果您發現本站中有涉嫌抄襲或描述失實的內容,請聯系我們jiasou666@gmail.com 處理,核實后本網站將在24小時內刪除侵權內容。

      上一篇:大前端課程學習心得體會+學習筆記
      下一篇:深入理解Win認證分享
      相關文章
      国产亚洲?V无码?V男人的天堂| 亚洲AV无码XXX麻豆艾秋| 亚洲av日韩专区在线观看| 亚洲性色高清完整版在线观看| 亚洲精品国产精品乱码视色| 亚洲免费一区二区| 亚洲色一色噜一噜噜噜| 免费观看亚洲人成网站| 亚洲а∨精品天堂在线| 亚洲国产精品嫩草影院| 亚洲精华液一二三产区| 亚洲熟妇久久精品| 中文字幕在线日亚洲9| 亚洲综合一区无码精品| 亚洲午夜无码久久久久小说| 亚洲乱码在线卡一卡二卡新区| 亚洲a级成人片在线观看| 2019亚洲午夜无码天堂| 亚洲熟女综合色一区二区三区| 亚洲人成777在线播放| 国产精品高清视亚洲一区二区 | 国产成人A亚洲精V品无码| 久久精品国产亚洲5555| 亚洲精品无码专区在线在线播放| 精品亚洲综合久久中文字幕| 亚洲处破女AV日韩精品| 亚洲狠狠婷婷综合久久久久| 亚洲阿v天堂在线| 亚洲资源在线观看| 亚洲专区中文字幕| 日韩亚洲国产综合高清| 亚洲人成色777777老人头| 亚洲AV无码一区二区三区网址| 国产成人va亚洲电影| 亚洲精品无码久久久| 亚洲国产日韩在线视频| 亚洲综合色丁香麻豆| 亚洲情A成黄在线观看动漫软件| 亚洲国产欧洲综合997久久| 亚洲?V无码乱码国产精品| 亚洲色精品88色婷婷七月丁香|