併發編程之Java內存模型

5.1 Java內存模型

JMM即Java Memory Model,它定義了主存、工做內存抽象概念,底層對應着CPU寄存器、緩存、硬件內存、CPU指令優化等。
JMM體如今如下幾個方面編程

  • 原子性 - 保證指令不會受到線程上下文切換的影響
  • 可見性 - 保證指令不會受cput緩存的影響
  • 有序性 - 保證指令不會受cpu指令並行優化的影響

5.2 可見性

退不出的循環
先來看一個現象,main線程對run變量的修改對於t線程不可見,致使了t線程沒法中止 :
在這裏插入圖片描述
爲何呢?分析一下 :
1.初始狀態,t線程剛開始從主內存讀取了run的值到工做內存。
在這裏插入圖片描述
2. 由於t線程要頻繁從主內存中讀取run的值,JIT編譯器會將run的值緩存至本身工做內存中的高速緩存中,減小對主存中run的訪問,提升效率
在這裏插入圖片描述
3. 1秒以後,main線程修改了run的值,並同步至主存,而t是從本身工做內存中的高速緩存中讀取這個變量的值,結果永遠是舊值
在這裏插入圖片描述
解決方法
volatile(易變關鍵字)
它能夠用來修飾成員變量和靜態成員變量,它能夠避免線程從本身的工做緩存中查找變量的值,必須到主存中獲取它的值,線程操做volatile變量都是直接操做主存。
可見性 VS 原子性
前面例子體現的實際就是可見性,它保證的是在多個線程之間,一個線程對volatile變量的修改對另外一個線程可見,不能保證原子性,僅用在一個寫線程,多個讀線程的狀況 :
上例從字節碼理解是這樣的 :
在這裏插入圖片描述
比較一下以前咱們將線程安全時舉的例子 :兩個線程一個i++ 一個i–,只能保證看到最新值,不能解決指令交錯
在這裏插入圖片描述
注意
synchronized語句塊既能夠保證代碼塊的原子性,也同時保證代碼塊內變量的可見性。但缺點是synchronized是屬於重量級操做,性能相對更低。
若是在前面示例中的死循環中加入System.out.println()會發現即便不加volatile修飾符,線程t也能正確看到對run變量的修改了,想想爲何?緩存

5.3 有序性

JVM會在不影響正確性的前提下,能夠調整語句的執行順序 :
在這裏插入圖片描述
能夠看到,至因而先執行i仍是先執行j,對最終的結果不會產生影響。因此,上面代碼真正執行時,既能夠是
在這裏插入圖片描述
也能夠是
在這裏插入圖片描述
這種特性稱之爲指令重排,多線程下指令重排會影響正確性。安全

volatile原理

volatile的底層實現原理是內存屏障,Memory Barrier(Memory Fence)多線程

  • 對volatile變量的寫指令後後加入寫屏障
  • 對volatile變量的讀指令前會加入讀屏障
  1. 如何保證可見性
  • 寫屏障(sfence)保證在該屏障以前的,對共享變量的改動,都同步到主存當中
    在這裏插入圖片描述
  • 而讀屏障(lfence)保證在該屏障以後,對共享變量的讀取,加載的是主存中最新的數據
    在這裏插入圖片描述
    在這裏插入圖片描述
    2.如何保證有序性
  • 寫屏障會確保指令重排序時,不會將寫屏障以前的代碼排在寫屏障以後
    在這裏插入圖片描述
  • 讀屏障會確保指令重排序時,不會將讀屏障以後的代碼排在讀屏障以前
    在這裏插入圖片描述
    在這裏插入圖片描述
    不能解決指令交錯 :
  • 寫屏障僅僅是保證以後的讀可以讀到最新的結果,但不能保證讀跑到它前面去
  • 而有序性的保證也只是保證了本線程內相關代碼不被重排序
    在這裏插入圖片描述

double-checked locking 單例模式爲例

在這裏插入圖片描述
以上的實現特色是 :併發

  • 懶惰實例化
  • 首次使用getInstance()才使用synchronized加鎖,後續使用時無需加鎖
  • 有隱含的,但很關鍵的一點 : 第一個if使用了INSTANCE變量,是在同步塊以外
    但在多線程環境下,上面的代碼是有問題的,getInstance方法對應的字節碼爲 :
    在這裏插入圖片描述
    其中
  • 17表示建立對象,將對象引用入棧 // new Singleton
  • 20表示複製一份對象引用 // 引用地址
  • 21表示利用一個對象引用,調用構造方法 // 根據引用地址調用
  • 24表示利用一個對象引用,賦值給 static INSTANCE
    也許jvm會優化爲 : 先執行24,再執行21.若是兩個線程t1,t2按以下時間序列執行 :
    在這裏插入圖片描述
    關鍵在於 0 :getstatic這行代碼在monitor控制以外,它就像以前舉例中不守規則的人,能夠越過monitor讀取INSTANCE變量的值
    這時t1還未完成將構造方法執行完畢,若是在構造方法中要執行不少初始化操做,那麼t2拿到的是將是一個未初始化完畢的單例
    對INSTANCE使用volatile修飾便可,能夠禁用指令重排,但要注意在JDK5以上版本的volatile纔會真正有效

4.double-checked locking 解決

在這裏插入圖片描述
字節碼上看不出來volatile指令的效果
在這裏插入圖片描述
在這裏插入圖片描述
happens-before
happens-before規定了對共享變量的寫操做對其它線程的讀操做可見,它是可見性與有序性的一套規則總結,拋開如下happens-before規則,JMM並不能保證一個線程對共享變量的寫,對於其它線程對該共享變量的讀可見app

  • 線程解鎖m以前對變量的寫,對於接下來對m加鎖的其它線程對該變量的讀可見
    在這裏插入圖片描述
  • 線程對volatile變量的寫,對接下來其它線程對該變量的讀可見
    在這裏插入圖片描述
  • 線程start前對變量的寫,對該線程開始後對該變量的讀可見
    在這裏插入圖片描述
  • 線程結束前對變量的寫,對其它線程得知它結束後的讀可見(好比其它線程調用t1.isAlive()或t1.join()等待它結束)
    在這裏插入圖片描述
  • 線程t1打斷t2(interrupt)前對變量的寫,對於其餘線程得知t2被打斷後對變量的讀可見(經過t2.interrupted或t2.interrupted)
    在這裏插入圖片描述
  • 對變量默認值(0,false,null)的寫,對其它線程對該變量的讀可見
  • 具備傳遞性,若是x hb -> z 那麼有x hb -> z,配合volatile的防指令重排,有下面的例子
    在這裏插入圖片描述變量都是指成員變量或靜態成員變量
相關文章
相關標籤/搜索