JMM小記

關(guān)于JMM的思考

前言

看《Java并發(fā)編程的藝術(shù)》總在思考一個問題,JMM到底是個什么東西?我們又需要JMM來討論什么問題?JMM中規(guī)定的happens-before規(guī)則到底決定了什么,有什么意義?

然而思考了很久,礙于水平有限并不能完全清楚的解答這一系列的問題,但還是決定將最近的一點思考記錄下來。萬一以后想明白了呢。

那么一個疑問就是關(guān)于JMM本身

為什么會有JMM

大致可以這么理解,并發(fā)問題的本質(zhì)是應該串行的方法并行執(zhí)行了,并操作了不該操作的數(shù)據(jù)導致了錯誤的結(jié)果。所以設計了悲觀鎖的機制,讓對臨界區(qū)資源操作的并發(fā)程序退化成串行執(zhí)行。(理解意思即可)

所以引申出并發(fā)編程的兩個關(guān)鍵問題

第一個問題

當我們評價一個多線程的程序時,第一個想起的總是線程是否安全,那么線程安全到底是指什么?

思考一個最常見的線程安全問題,方法A,B都會訪問同樣的資源。方法A先執(zhí)行,方法B再執(zhí)行,當A未執(zhí)行完成B就讀取了臨界區(qū)的值,導致了不安全情況的發(fā)生。

那么精確的形容這個問題,其實就是不同線程因為都存在對臨界區(qū)的操作而導致程序必須控制不同線程操作發(fā)生的相對順序,也就是線程的同步問題

第二個問題

正常的多線程程序中,一般通過共享內(nèi)存來實現(xiàn)線程之間的信息交換,而這實際就是在解決并發(fā)編程的通信問題


所以多線程技術(shù)討論的核心都是這兩個問題,但這一切可以實現(xiàn)的基礎是要求:

  1. 先寫的代碼運行結(jié)果,之后的代碼是一定可見的

  2. 代碼的運行順序是和我們所書寫順序一樣的

但事與愿違,簡單的認為之前寫的代碼結(jié)果一定可以被之后的代碼感知是錯誤的,因為計算機底層的復雜實現(xiàn),存在緩存。寫入的代碼不一定刷到了主存中,而讀取的那一方也可能直接從緩存中讀取而不經(jīng)過主存。

同樣的代碼在處理器上的最終執(zhí)行順序也并不會和書寫順序一致。

總結(jié)上面兩點,也就是:

  1. 先后運行的代碼,多線程中不一定是內(nèi)存可見的

  2. 代碼執(zhí)行的順序一定和書寫的順序不同

而這一切其實都是底層的硬件實現(xiàn)所導致的。所以為了,程序員可以忽略底層細節(jié)而快速方便的討論數(shù)據(jù)的內(nèi)存狀態(tài)(討論上述兩個問題),設計出了JMM這種抽象模型,它規(guī)定了Java程序運行時數(shù)據(jù)可能存在的內(nèi)存狀態(tài),也定義了在內(nèi)存級別下數(shù)據(jù)的原子性操作。同時可以看出JMM所討論的問題正是多線程技術(shù)實現(xiàn)的基礎。

那么在此基礎上來分析JMM中討論的兩個核心問題

JMM中為什么要討論內(nèi)存可見

JMM抽象示意圖如下:

JMM.png

從圖可以得知,如果線程A,B之間需要通信,那么必須要經(jīng)歷如下兩個步驟:

  1. 線程A把本地內(nèi)存A中更新過共享變量刷新到主內(nèi)存中

  2. 線程B到主內(nèi)存中讀取線程A之前更新過的共享變量

這里也就說明了,如果希望程序正確的運行,共享變量內(nèi)存可見性是十分重要的(如果A修改了某個值,B隨后讀取,但因為沒有將本地內(nèi)存中的值刷會主內(nèi)存,而導致應該讀到的值沒有讀到)。通常情況下,從A本地內(nèi)存寫道主內(nèi)存再讀到B的本地內(nèi)存不是一個原子操作。

JMM中為什么要討論重排序

開始為了處理多線程帶來的問題,一般會想到讓多線程退化成單線程,也就是悲觀鎖。當然對于悲觀鎖而言重排序沒有任何的討論意義,在保證內(nèi)存可見的情況下,上一個獲得鎖對臨界區(qū)的操作一定是對下一個鎖可見的,無論上一個鎖內(nèi)的指令執(zhí)行順序如何,因為對當前獲得鎖的線程而言之前方法的操作都全部完成了順序根本沒有意義,而且JMM模型是允許在悲觀鎖內(nèi)進行重排序的。

JMM討論重排序是處于樂觀鎖的實現(xiàn)必要,因為悲觀鎖性能的性能問題,JUC包中的一切都是以CAS及樂觀鎖的思想進行實現(xiàn)的。這種實現(xiàn)本質(zhì)是允許多個線程同時操作臨界區(qū)的,只在關(guān)鍵步驟進行CAS操作進行檢查,所以多個線程內(nèi)各自指令的操作順序就變得重要且有意義了,Java本地方法的CAS系列操作都通過內(nèi)存屏障實現(xiàn)了volatile語義的讀和寫,保證了指令不被重排序樂觀鎖才有可能實現(xiàn)。

下面的例子也可以說明重排序的問題本質(zhì)還是破壞了多線程的內(nèi)存語義,導致了內(nèi)存不可見。

另外在假設代碼中每個讀寫操作都是原子的(也就是操作立即可見)情況下,重排序仍然會破壞內(nèi)存的可見性。

代碼在實際運行時并不是按書寫的順序執(zhí)行的。為了提高性能,編譯器和處理器通常會對指令做重排序(編譯器,指令級并行,內(nèi)存系統(tǒng)三種),在單線程下系統(tǒng)可以自行檢查代碼之間的依賴關(guān)系,沒有依賴關(guān)系的可以被重排序。在多線程下,線程之間代碼的依賴關(guān)系顯然已經(jīng)不可能由系統(tǒng)完成,觀察如下代碼。

class ReorderExample{
    int a = 0;
    boolean flag = false;
    
    public void writer(){
        a = 1;          //1
        flag =true;     //2
    }
    
    public void  reader(){
        if(flag){       //3
            int i = a * a;//4
        }
    }
}

線程B在操作4時是不一定可以看到線程A對a的寫入的。因為在線程A中1,2操作并沒有依賴關(guān)系,被允許重排了,而B進入判斷條件后a還沒有被賦值,并無法感知到1隨后對a的修改。所以即使這里保證了內(nèi)存被即使刷會主存且強制讀取,重排序還是破壞了多線程的語義

一些同樣重要的概念

Volatile 在JMM中有多重要

volatile規(guī)定了變量如何保證可見性,同時對一個變量的讀或?qū)懕WC為原子性。也就是JMM討論的核心問題之一,內(nèi)存的可見性就是以volatile為代表,因為volatile是對于內(nèi)存可見性的最小實現(xiàn),所以討論其他操作的內(nèi)存語義時都以volatile進行比較。

而volatile的語義又是通過內(nèi)存屏障來保證的,JMM中討論的內(nèi)存 屏障也經(jīng)過了簡化,它對編譯器和處理器發(fā)出內(nèi)存屏障的指令,但具體實現(xiàn)取決于不同的硬件設備。

Happens-before

學習JMM難免會困惑happens-before到底是個啥?舉例中總會說到volatile的寫/讀實現(xiàn)了happens-before關(guān)系,還是十分令人疑惑。

但可以明確的是Happens-before是一套形容操作間內(nèi)存可見關(guān)系的規(guī)則,是JMM為了屏蔽底層的硬件細節(jié)(如重排序)而通過volatile, lock等方法和工具為程序員提供的一種便于理解內(nèi)存可見性的手段。

Happens-before定義了8種規(guī)則,這些規(guī)則都是具有已有的具體實現(xiàn)上總結(jié)出的內(nèi)存可見規(guī)則,但說XXX建立了Happens-before規(guī)則就可以簡單快速的了解操作間的內(nèi)存可見情況

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

友情鏈接更多精彩內(nèi)容