深刻彙編指令理解Java關鍵字volatile

volatile是什麼

volatile關鍵字是Java提供的一種輕量級同步機制。它可以保證可見性和有序性,可是不能保證原子性html

可見性

對於volatile的可見性,先看看這段代碼的執行程序員

  • flag默認爲true
  • 建立一個線程A去判斷flag是否爲true,若是爲true循環執行i++操做
  • 兩秒後,建立另外一個線程B將flag修改成false
  • 線程A沒有感知到flag已經被修改爲false了,不能跳出循環

這至關於啥呢?至關於你的女神和你說,你好好努力,年薪百萬了就嫁給你,你聽了以後,努力賺錢。3年以後,你年薪百萬了,回去找你女神,結果發現你女神結婚了,她結婚的消息根本沒有告訴你!難不難受?面試

女神結婚能夠不告訴你,但是Java代碼中的屬性都是存在內存中,一個線程的修改成什麼另外一個線程爲何不可見呢?這就不得不提到Java中的內存模型了,Java中的內存模型,簡稱JMM,JMM定義了線程和主內存之間的抽象關係,定義了線程之間的共享變量存儲在主內存中,每一個線程都有一個私有的本地內存,本地內存中存儲了該線程以讀/寫共享變量的副本,它涵蓋了緩存、寫緩衝區、寄存器以及其餘的硬件和編譯器優化。編程

注意!JMM是一個屏蔽了不一樣操做系統架構的差別的抽象概念,只是一組Java規範。緩存

瞭解了JMM,如今咱們再回顧一下文章開頭的那段代碼,爲何線程B修改了flag線程A看到的仍是原來的值呢?架構

  • 由於線程A複製了一份剛開始的flage=true到本地內存,以後線程A使用的flag都是這個複製到本地內存的flag。
  • 線程B修改了flag以後,將flag的值刷新到主內存,此時主內存的flag值變成了false
  • 線程A是不知道線程B修改了flag,一直用的是本地內存的flag = true

那麼,如何才能讓線程A知道flag被修改了呢?或者說怎麼讓線程A本地內存中緩存的flag無效,實現線程間可見呢?用volatile修飾flag就能夠作到:併發

咱們能夠看到,用volatile修飾flag以後,線程B修改flag以後線程A是能感知到的,說明了volatile保證了線程同步之間的可見性。app

重排序

在闡述volatile有序性以前,須要先補充一些關於重排序的知識。性能

重排序是指編譯器和處理器爲了優化程序性能而對指令序列進行從新排序的一種手段。優化

爲何要有重排序呢?簡單來講,就是爲了提高執行效率。爲何能提高執行效率呢?咱們看下面這個例子:

能夠看到重排序以後CPU實際執行省略了一個讀取和寫回的操做,也就間接的提高了執行效率。

有一點必須強調的是,上圖的例子只是爲了讓讀者更好的理解爲何重排序能提高執行效率,實際上Java裏面的重排序並非基於代碼級別的,從代碼到CPU執行之間還有不少個階段,CPU底層還有一些優化,實際上的執行流程可能並非上圖的說的那樣。沒必要過於糾結於此。

重排序能夠提升程序的運行效率,可是必須遵循as-if-serial語義。as-if-serial語義是什麼呢?簡單來講,就是無論你怎麼重排序,你必須保證無論怎麼重排序,單線程下程序的執行結果不能被改變。

有序性

上面咱們已經介紹了Java有重排序狀況,如今咱們再來聊一聊volatile的有序性。

先看一個經典的面試題:爲何DDL(double check lock)單例模式須要加volatile關鍵字?

由於singleton = new Singleton()不是一個原子操做,大概要通過這幾個步驟:

  • 分配一塊內存空間
  • 調用構造器,初始化實例
  • singleton指向分配的內存空間

實際執行的時候,可能發生重排序,致使實際執行步驟是這樣的:

  • 申請一塊內存空間
  • singleton指向分配的內存空間
  • 調用構造器,初始化實例

singleton指向分配的內存空間以後,singleton就不爲空了。可是在沒有調用構造器初始化實例以前,這個對象還處於半初始化狀態,在這個狀態下,實例的屬性都仍是默認屬性,這個時候若是有另外一個線程調用getSingleton()方法時,會拿到這個半初始化的對象,致使出錯。

而加volatile修飾以後,就會禁止重排序,這樣就能保證在對象初始化完了以後才把singleton指向分配的內存空間,杜絕了一些不可控錯誤的產生。volatile提供了happens-before保證,對volatile變量的寫入happens-before全部其餘線程後續對的讀操做。

原理

從上面的DDL單例用例來看,在併發狀況下,重排序的存在會致使一些未知的錯誤。而加上volatile以後會防止重排序,那volatile是如何禁止重排序呢?

爲了實現volatile的內存語義,JMM會限制特定類型的編譯器和處理器重排序,JMM會針對編譯器制定volatile重排序規則表:

總結來講就是:

  • 第二個操做是volatile寫,無論第一個操做是什麼都不會重排序
  • 第一個操做是volatile讀,無論第二個操做是什麼都不會重排序
  • 第一個操做是volatile寫,第二個操做是volatile讀,也不會發生重排序

如何保證這些操做不會發送重排序呢?就是經過插入內存屏障保證的,JMM層面的內存屏障分爲讀(load)屏障和寫(Store)屏障,排列組合就有了四種屏障。對於volatile操做,JMM內存屏障插入策略:

  • 在每一個volatile寫操做的前面插入一個StoreStore屏障
  • 在每一個volatile寫操做的後面插入一個StoreLoad屏障
  • 在每一個volatile讀操做的後面插入一個LoadLoad屏障
  • 在每一個volatile讀操做的後面插入一個LoadStore屏障

上面的屏障都是JMM規範級別的,意思是,按照這個規範寫JDK能保證volatile修飾的內存區域的操做不會發送重排序。

在硬件層面上,也提供了一系列的內存屏障來提供一致性的能力。拿X86平臺來講,主要提供了這幾種內存屏障指令:

  • lfence指令:在lfence指令前的讀操做當必須在lfence指令後的讀操做前完成,相似於讀屏障
  • sfence指令:在sfence指令前的寫操做當必須在sfence指令後的寫操做前完成,相似於寫屏障
  • mfence指令: 在mfence指令前的讀寫操做當必須在mfence指令後的讀寫操做前完成,相似讀寫屏障。

JMM規範須要加這麼多內存屏障,但實際狀況並不須要加這麼多內存屏障。以咱們常見的X86處理器爲例,X86處理器不會對讀-讀讀-寫寫-寫操做作重排序,會省略掉這3種操做類型對應的內存屏障,僅會對寫-讀操做作重排序。因此volatile寫-讀操做只須要在volatile寫後插入StoreLoad屏障。在《The JSR-133 Cookbook for Compiler Writers》中,也很明確的指出了這一點:

而在x86處理器中,有三種方法能夠實現實現StoreLoad屏障的效果,分別爲:

  • mfence指令:上文提到過,能實現全能型屏障,具有lfence和sfence的能力。
  • cpuid指令:cpuid操做碼是一個面向x86架構的處理器補充指令,它的名稱派生自CPU識別,做用是容許軟件發現處理器的詳細信息。
  • lock指令前綴:總線鎖。lock前綴只能加在一些特殊的指令前面。

實際上HotSpot關於volatile的實現就是使用的lock指令,只在volatile標記的地方加上帶lock前綴指令操做,並無參照JMM規範的屏障設計而使用對應的mfence指令。

加上-XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly -XcompJVM參數再次執行main方法,在打印的彙編碼中,咱們也能夠看到有一個lock addl $0x0,(%rsp)的操做。

在源碼中也能夠獲得驗證:

lock addl $0x0,(%rsp)後面的addl $0x0,(%rsp)實際上是一個空操做。add是加的意思,0x0是16進制的0,rsp是一種類型寄存器,合起來就是把寄存器的值加0,加0是否是等於什麼都沒有作?這段彙編碼僅僅是lock指令的一個載體而已。其實上文也有提到過,lock前綴只能加在一些特殊的指令前面,add就是其中一個指令。

至於Hotspot爲何要使用lock指令而不是mfence指令,按照個人理解,其實就是省事,實現起來簡單。由於lock功能過於強大,不須要有太多的考慮。並且lock指令優先鎖緩存行,在性能上,lock指令也沒有想象中的那麼差,mfence指令更沒有想象中的好。因此,使用lock是一個性價比很是高的一個選擇。並且,lock也有對可見性的語義說明。

在《IA-32架構軟件開發人員手冊》的指令表中找到lock:

我不打算在這裏深刻闡述lock指令的實現原理和細節,這很容易陷入堆砌技術術語中,並且也超出了本文的範圍,有興趣的能夠去看看《IA-32架構軟件開發人員手冊》。

咱們只須要知道lock的這幾個做用就能夠了:

  • 確保後續指令執行的原子性。在Pentium及以前的處理器中,帶有lock前綴的指令在執行期間會鎖住總線,使得其它處理器暫時沒法經過總線訪問內存,很顯然,這個開銷很大。在新的處理器中,Intel使用緩存鎖定來保證指令執行的原子性,緩存鎖定將大大下降lock前綴指令的執行開銷。
  • 禁止該指令與前面和後面的讀寫指令重排序。
  • 把寫緩衝區的全部數據刷新到內存中。

總結來講,就是lock指令既保證了可見性也保證了原子性。

重要的事情再說一遍,是lock指令既保證了可見性也保證了原子性,和什麼緩衝一致性協議啊,MESI什麼的沒有一點關係。

爲了避免讓你把緩存一致性協議和JMM混淆,在前面的文章中,我特地沒有提到過緩存一致性協議,由於這二者本不是一個維度的東西,存在的意義也不同,這一部分,咱們下次再聊。

總結

全文重點是圍繞volatile的可見性和有序性展開的,其中花了很多的部分篇幅描述了一些計算機底層的概念,對於讀者來講可能過於無趣,但若是你能認真看完,我相信你或多或少也會有一點收穫。

不去深究,volatile只是一個普通的關鍵字。深刻探討,你會發現volatile是一個很是重要的知識點。volatile能將軟件和硬件結合起來,想要完全弄懂,須要深刻到計算機的最底層。但若是你作到了。你對Java的認知必定會有進一步的提高。

只把眼光放在Java語言,彷佛顯得很是侷限。發散到其餘語言,C語言,C++裏面也都有volatile關鍵字。我沒有看過C語言,C++裏面volatile關鍵字是如何實現的,但我相信底層的原理必定是相通的。

寫在最後

本着對每一篇發出去的文章負責的原則,文中涉及知識理論,我都會盡可能在官方文檔和權威書籍找到並加以驗證。但即便這樣,我也不能保證文中每一個點都是正確的,若是你發現錯誤之處,歡迎指出,我會對其修正。

創做不易,你的正反饋對我來講很是重要!點個贊,點個再看,點個關注甚至評論區發送一條666都是對我最大的支持!

我是CoderW,一個普通的程序員。

謝謝你的閱讀,咱們下期再見!

參考資料

相關文章
相關標籤/搜索