1.背景JMM(Java Memory Model)的提出,主要基於以下的幾種原因:不同操作系統平台的內存模型不同,而Java又想做到Write Once Run Everywhere(即跨平台),那麼必須要自己提供一套內存模型以屏蔽不同操作系統在內存模型方面的差異。由於除了編譯器層面可以進行指令重排外,處理器層面也可以,儘管指令重排在一定程度上能夠提升程序運行的效率,但這僅限於單線程環境下,一旦處在多線程環境,這種指令重排帶來的更多的是併發性問題(比如多級緩存與內存中的數據不一致),而Java又是多線程語言,那麼就會存在多線程編程。為了保證開發者編寫的併發程序能夠按照預期安全地執行,Java必須提出一套併發編程相關的規範(比如happens-before原則)來解決多線程環境下的可能存在的併發性問題。2.內容2.1 內存規範2.1.1 內存抽象
本地/工作內存:每個線程私有且獨佔,不可跨線程訪問的物理內存區域。存放了共享變量的副本。主內存:線程間共享的邏輯內存區域(並不真實存在)。存放所有線程創建的實例對象,包括:成員變量、局部變量、類信息、常量、靜態變量等。線程間通信必須藉助主內存來進行。2.1.2 內存操作
下面的8種操作主要用於主內存與工作內存的數據交互:鎖定(lock):將主內存中的一個共享變量標記為線程獨享變量。讀取(read):將主內存中的一個共享變量傳遞到線程的工作內存中。載入(load):將從主存(通過read)讀到的共享變量裝載進工作內存中的相應的共享變量副本中。使用(use):將共享變量的副本傳遞給執行引擎,供其使用。(每次需要用到該變量時,都會執行該指令)賦值(assign):將從執行引擎接收到的結果值賦值給共享內存的副本。(每次的賦值操作均會調用該指令)存儲(store):將共享變量的副本傳遞迴主內存中。寫入(write):將接收到的共享變量的副本(新修改的共享變量)寫回主內存中的相應的共享變量中。解鎖(unlock):將主內存中的一個共享變量的線程獨享變量的狀態標記取消。注:
JMM規範中並沒有明確説明read和store將讀到的共享變量(副本)存放在工作內存/主內存的什麼地方,但是肯定沒有存發在共享變量(副本)對應的槽位上(類似於找一個地方暫存一下數據的意思),因為它需要load和write指令正式存放到相應的變量槽位上去。要求:同一時間,只能有一個線程持有某一共享變量的鎖(通過lock指令獲取),獲得鎖的線程可以多次加鎖(同一個),但是釋放鎖時需要釋放同等次數(類似於ReentrantLock)。使用共享變量之前,必須先讀取並載入,即不允許在工作內存中讀取一個憑空誕生(主內存中沒有)的變量。不允許寫回一個沒有賦值的共享變量副本。(這樣做你覺得有意義?😅)......(此處省略很多字)2.2 併發規範2.2.1 happends-before原則一句話概括就是:前一個操作的結果對後一個操作是可見的,無論這兩個操作是否處在同一個線程裏。8項原則:程序順序原則:在同一個線程內的代碼中,寫在前面的代碼happens-before寫在後面的代碼。鎖規則:加鎖happens-before釋放鎖。volatile規則:對於一個volatile變量的寫操作happens-before volatile變量的讀操作。(該原則並不是説你只能先寫後讀一個volatile變量,而是説,前一個操作如果是寫入volatile變量的操作,那麼該volatile變量的新值對於後續的volatile讀操作一定是可見的)start規則:線程的start()動作happens-before線程內的每個動作。join規則:線程內的所有動作happens-before線程的join()動作。interrupt規則:對線程調用interrupt() happens-before線程自己檢測到中斷事件的發生。finalize規則:一個對象的構造函數的執行、結束和返回happens-before這個對象finalize()方法的開始。傳遞性:A happens-beforeB,B happens-before C,那麼就有A happens-before C。3.注意事項3.1 JMM與JVM內存結構的區別JMM:與Java併發編程相關,抽象了線程與主內存的關係,規定了併發相關的原則和規範,以簡化多線程編程,增強程序的可移植性。JVM內存結構:與JVM運行時區域相關,定義了JVM如何分區存儲不同類型的數據。3.2 happens-before與JMM的關係
JMM通過自行禁用處理器的若干重排序規則或者直接禁用某種類型的處理器來實現它向程序猿發出的happens-before幾點保證,因為處理器的重排序規則在併發環境下可能會產生未知差錯從而使得happens-before失去保證,所以必須要根據實際的運行情況進行適當的禁用。4.補充4.1 併發編程的三大重要特性原子性:一次操作或者多個操作要麼全都執行,要麼全都不執行。(這裏不包括執行失敗後回滾的情況,即併發編程的原子性 ≠ 事務性的原子性)可見性:當一個線程對某一個共享變量進行了修改,那麼其他線程將能夠立即看到修改後的新值。有序性:因為編譯器和處理器對代碼進行指令重排序的緣故,你所編寫的代碼的順序不一定就是代碼實際的執行順序。參考文檔JMM(Java 內存模型)詳解