java內存模型JMM理解整理

  什麼是JMM

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

  內存劃分

  JMM規定了內存主要劃分爲主內存和工做內存兩種。此處的主內存和工做內存跟JVM內存劃分(堆、棧、方法區)是在不一樣的層次上進行的,若是非要對應起來,主內存對應的是Java堆中的對象實例部分,工做內存對應的是棧中的部分區域,從更底層的來講,主內存對應的是硬件的物理內存,工做內存對應的是寄存器和高速緩存。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都還沒有被賦值的結果。app

  可見性:每一個工做線程都有本身的工做內存,因此當某個線程修改完某個變量以後,在其餘的線程中,未必能觀察到該變量已經被修改。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中的規則。經過這些條件的斷定,仍然很難判斷一個線程是否能安全執行,畢竟在咱們的時候線程安全多數依賴於工具類的安全性來保證。想提升本身對線程是否安全的判斷能力,必然須要理解所使用的框架或者工具的實現,並積累線程安全的經驗。

相關文章
相關標籤/搜索