JMM理解整理

java內存模型JMM理解整理

  什麼是JMM

  JMM即爲JAVA 內存模型(java memory model)。因爲在不同的硬件生產商和不同的操作系統下,內存的訪問邏輯有一定的差異,結果就是當你的代碼在某個系統環境下運行良好,並且線程安全,但是換了個系統就出現各種問題。Java內存模型,就是爲了屏蔽系統和硬件的差異,讓一套代碼在不同平臺下能到達相同的訪問結果。JMM從java 5開始的JSR-133發佈後,已經成熟和完善起來。

  內存劃分

  JMM規定了內存主要劃分爲主內存和工作內存兩種。此處的主內存和工作內存跟JVM內存劃分(堆、棧、方法區)是在不同的層次上進行的,如果非要對應起來,主內存對應的是Java堆中的對象實例部分,工作內存對應的是棧中的部分區域,從更底層的來說,主內存對應的是硬件的物理內存,工作內存對應的是寄存器和高速緩存。

  JVM在設計時候考慮到,如果JAVA線程每次讀取和寫入變量都直接操作主內存,對性能影響比較大,所以每條線程擁有各自的工作內存,工作內存中的變量是主內存中的一份拷貝,線程對變量的讀取和寫入,直接在工作內存中操作,而不能直接去操作主內存中的變量。但是這樣就會出現一個問題,當一個線程修改了自己工作內存中變量,對其他線程是不可見的,會導致線程不安全的問題。因爲JMM制定了一套標準來保證開發者在編寫多線程程序的時候,能夠控制什麼時候內存會被同步給其他線程。

  內存交互操作

   內存交互操作有8種,虛擬機實現必須保證每一個操作都是原子的,不可在分的(對於double和long類型的變量來說,load、store、read和write操作在某些平臺上允許例外)

    • lock     (鎖定):作用於主內存的變量,把一個變量標識爲線程獨佔狀態
    • unlock (解鎖):作用於主內存的變量,它把一個處於鎖定狀態的變量釋放出來,釋放後的變量纔可以被其他線程鎖定
    • read    (讀取):作用於主內存變量,它把一個變量的值從主內存傳輸到線程的工作內存中,以便隨後的load動作使用
    • load     (載入):作用於工作內存的變量,它把read操作從主存中變量放入工作內存中
    • use      (使用):作用於工作內存中的變量,它把工作內存中的變量傳輸給執行引擎,每當虛擬機遇到一個需要使用到變量的值,就會使用到這個指令
    • assign  (賦值):作用於工作內存中的變量,它把一個從執行引擎中接受到的值放入工作內存的變量副本中
    • store    (存儲):作用於主內存中的變量,它把一個從工作內存中一個變量的值傳送到主內存中,以便後續的write使用
    • write  (寫入):作用於主內存中的變量,它把store操作從工作內存中得到的變量的值放入主內存的變量中

  JMM對這八種指令的使用,制定瞭如下規則:

    • 不允許read和load、store和write操作之一單獨出現。即使用了read必須load,使用了store必須write
    • 不允許線程丟棄他最近的assign操作,即工作變量的數據改變了之後,必須告知主存
    • 不允許一個線程將沒有assign的數據從工作內存同步回主內存
    • 一個新的變量必須在主內存中誕生,不允許工作內存直接使用一個未被初始化的變量。就是懟變量實施use、store操作之前,必須經過assign和load操作
    • 一個變量同一時間只有一個線程能對其進行lock。多次lock後,必須執行相同次數的unlock才能解鎖
    • 如果對一個變量進行lock操作,會清空所有工作內存中此變量的值,在執行引擎使用這個變量前,必須重新load或assign操作初始化變量的值
    • 如果一個變量沒有被lock,就不能對其進行unlock操作。也不能unlock一個被其他線程鎖住的變量
    • 對一個變量進行unlock操作之前,必須把此變量同步回主內存

  JMM對這八種操作規則和對volatile的一些特殊規則就能確定哪裏操作是線程安全,哪些操作是線程不安全的了。但是這些規則實在複雜,很難在實踐中直接分析。所以一般我們也不會通過上述規則進行分析。更多的時候,使用java的happen-before規則來進行分析。

  模型特徵

  原子性:例如上面八項操作,在操作系統裏面是不可分割的單元。被synchronized關鍵字或其他鎖包裹起來的操作也可以認爲是原子的。從一個線程觀察另外一個線程的時候,看到的都是一個個原子性的操作。

1         synchronized (this) {
2             a=1;
3             b=2;
4         }

  例如一個線程觀察另外一個線程執行上面的代碼,只能看到a、b都被賦值成功結果,或者a、b都尚未被賦值的結果。

  可見性:每個工作線程都有自己的工作內存,所以當某個線程修改完某個變量之後,在其他的線程中,未必能觀察到該變量已經被修改。volatile關鍵字要求被修改之後的變量要求立即更新到主內存,每次使用前從主內存處進行讀取。因此volatile可以保證可見性。除了volatile以外,synchronized和final也能實現可見性。synchronized保證unlock之前必須先把變量刷新回主內存。final修飾的字段在構造器中一旦完成初始化,並且構造器沒有this逸出,那麼其他線程就能看到final字段的值。

  有序性:java的有序性跟線程相關。如果在線程內部觀察,會發現當前線程的一切操作都是有序的。如果在線程的外部來觀察的話,會發現線程的所有操作都是無序的。因爲JMM的工作內存和主內存之間存在延遲,而且java會對一些指令進行重新排序。volatile和synchronized可以保證程序的有序性,很多程序員只理解這兩個關鍵字的執行互斥,而沒有很好的理解到volatile和synchronized也能保證指令不進行重排序。

  Volatile內存語義

   volatile的一些特殊規則

  Final域的內存語義

  被final修飾的變量,相比普通變量,內存語義有一些不同。具體如下:

    • JMM禁止把Final域的寫重排序到構造器的外部。
    • 在一個線程中,初次讀該對象和讀該對象下的Final域,JMM禁止處理器重新排序這兩個操作。

複製代碼

 1 public class FinalConstructor {
 2 
 3     final int a;
 4 
 5     int b;
 6 
 7     static FinalConstructor finalConstructor;
 8 
 9     public FinalConstructor() {
10         a = 1;
11         b = 2;
12     }
13 
14     public static void write() {
15         finalConstructor = new FinalConstructor();
16     }
17 
18     public static void read() {
19         FinalConstructor constructor = finalConstructor;
20         int A = constructor.a;
21         int B = constructor.b;
22     }
23 }

複製代碼

  假設現在有線程A執行FinalConstructor.write()方法,線程B執行FinalConstructor.read()方法。

  對應上述的Final的第一條規則,因爲JMM禁止把Final域的寫重排序到構造器的外部,而對普通變量沒有這種限制,所以變量A=1,而變量B可能會等於2(構造完成),也有可能等於0(第11行代碼被重排序到構造器的外部)。

   對應上述的Final的第二條規則,如果constructor的引用不爲null,A必然爲1,要麼constructor爲null,拋出空指針異常。保證讀final域之前,一定會先讀該對象的引用。但是普通對象就沒有這種規則。

  (上述的Final規則反覆測試,遺憾的是我並沒有能模擬出來普通變量不能正常構造的結果)

  Happen-Before(先行發生規則)

  在常規的開發中,如果我們通過上述規則來分析一個併發程序是否安全,估計腦殼會很疼。因爲更多時候,我們是分析一個併發程序是否安全,其實都依賴Happen-Before原則進行分析。Happen-Before被翻譯成先行發生原則,意思就是當A操作先行發生於B操作,則在發生B操作的時候,操作A產生的影響能被B觀察到,“影響”包括修改了內存中的共享變量的值、發送了消息、調用了方法等。

  Happen-Before的規則有以下幾條

    • 程序次序規則(Program Order Rule):在一個線程內,程序的執行規則跟程序的書寫規則是一致的,從上往下執行。
    • 管程鎖定規則(Monitor Lock Rule):一個Unlock的操作肯定先於下一次Lock的操作。這裏必須是同一個鎖。同理我們可以認爲在synchronized同步同一個鎖的時候,鎖內先行執行的代碼,對後續同步該鎖的線程來說是完全可見的。
    • volatile變量規則(volatile Variable Rule):對同一個volatile的變量,先行發生的寫操作,肯定早於後續發生的讀操作
    • 線程啓動規則(Thread Start Rule):Thread對象的start()方法先行發生於此線程的每一個動作
    • 線程中止規則(Thread Termination Rule):Thread對象的中止檢測(如:Thread.join(),Thread.isAlive()等)操作,必行晚於線程中所有操作
    • 線程中斷規則(Thread Interruption Rule):對線程的interruption()調用,先於被調用的線程檢測中斷事件(Thread.interrupted())的發生
    • 對象中止規則(Finalizer Rule):一個對象的初始化方法先於一個方法執行Finalizer()方法
    • 傳遞性(Transitivity):如果操作A先於操作B、操作B先於操作C,則操作A先於操作C

  以上就是Happen-Before中的規則。通過這些條件的判定,仍然很難判斷一個線程是否能安全執行,畢竟在我們的時候線程安全多數依賴於工具類的安全性來保證。想提高自己對線程是否安全的判斷能力,必然需要理解所使用的框架或者工具的實現,並積累線程安全的經驗。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章