【Java】Java內存模型

1、現代計算機內存模型

早期的計算機中因爲CPU和內存的速度是差很少的,因此CPU是直接訪問內存地址的。而在現代計算機中,CPU指令的運行速度遠遠超過了內存數據的讀寫速度,爲了下降這二者間這高達幾個數量級的差距,因此在CPU與主內存之間加入了CPU高速緩存。java

高速緩存能夠很好地解決CPU與主內存之間的速度差距,但CPU緩存並非全部CPU共享的,所以產生了一個新的問題:數據一致性問題程序員

2、緩存一致性協議(MESI)

CPU緩存的一致性問題會致使併發處理的不一樣步,對於這個問題,大概有如下兩種方案:編程

  1. 總線加鎖 ---> 下降了CPU的吞吐量,不現實
  2. 採用緩存上的一致性協議MESI ---> 現代處理器經常使用,或使用其變種的協議

1. MESI四種狀態

MESI 這個名稱自己是由Modified(修改)、Exclusive(獨享)、Shared(共享)、Invalid(無效)。這個四個單詞也表明了緩存協議中對緩存行(即Cache Line,緩存存儲數據的單元)聲明的四種狀態,用2 bit表示,它們所表明的含義以下所示:緩存

狀態 描述 監放任務
M修改(Modified) 這行數據有效,數據被修改了,和內存種的數據不一致,數據只存在於本Cache中 緩存行必須時刻監聽全部試圖讀該緩存行相對就主存的操做,這種操做必須在緩存將該緩存行寫回主存並將狀態變成S(共享)狀態以前被延遲執行。
E獨享(Exclusive) 這行數據有效,數據和內存中的數據一致,數據只存在於本Cache中 緩存行也必須監聽其它緩存讀主存中該緩存行的操做,一旦有這種操做,該緩存行須要變成S(共享)狀態。
S共享(Shared) 這行數據有效,數據和內存中的數據一致,數據存在於不少Cache中 緩存行也必須監聽其它緩存使該緩存行無效或者獨享該緩存行的請求,並將該緩存行變成無效(Invalid)。
I無效(Invalid) 這行數據無效
  • E狀態示例以下:多線程

    只有Core 0訪問變量x,它的Cache Line狀態爲E。併發

  • S狀態示例以下:app

    3個Core都訪問變量x,它們對應的Cache line爲S(Shared)狀態。性能

  • M狀態和I狀態示例以下:優化

    Core 0 修改了x的值以後,這個Cache Line變成了M(Modified)狀態,其餘Core對應的Cache line變成了I(Invalid)狀態。.net

2. 狀態間的遷移

在MESI協議中,每一個Cache的cache控制器不只知道本身的讀寫操做,並且也監聽其餘cache的讀寫操做。每一個Cache Line所處的狀態根據本核和其餘核的操做在4個狀態間進行遷移。

在上圖中,Local Read表示本內核讀本Cache中的值,Local Write表示本內核寫本Cache中的值,Remote Read表示其它內核讀其它Cache中的值,Remote Write表示其它內核寫其它Cache中的值,箭頭表示本Cache line狀態的遷移,環形箭頭表示狀態不變。

當內核須要訪問的數據不在本Cache中,而其它Cache有這份數據的備份時,本Cache既能夠從內存中導入數據,也能夠從其它Cache中導入數據,不一樣的處理器會有不一樣的選擇。

本文只進行簡單介紹,具體請閱讀Cache一致性協議之MESI

3. 如何保證緩存一致性

瞭解完什麼是MESI,那麼具體是如何保證緩存一致性的呢?

《Java併發編程的藝術》中提到:在多處理器下,爲了保證各個處理器的緩存是一致的,就會實現緩存一致性協議,每一個處理器經過嗅探在總線上傳播的數據來檢查本身緩存中的值是否是過時了,當處理器發現本身緩存行對應的內存地址被修改,就會將當前處理器的緩存行設置爲無效狀態,當處理器對這個數據進行修改操做的時候,會從新從系統內存中把數據讀處處理器緩存裏。

3、Java內存模型

1. Java內存模型的抽象結構

線程之間的共享變量存儲在主內存(Main Memory)中,每一個線程都有一個私有的本地工做內存(Local Memory),工做內存中存儲了線程以讀/寫共享變量的副本。(本地工做內存是 JMM 的一個抽象概念,並不真實存在,線程中所謂工做內存其實仍是存在於主內存中的。)

2. Java內存模型與現代計算機內存模型區分

Java內存模型和現代計算機內存模型都須要解決一致性問題,可是這個一致性問題在現代計算機內存模型中指代的是緩存一致性問題,MESI協議所設計的目的也是爲了解決這個問題。而在Java內存模型中,這個一致性問題則是指代內存一致性問題。二者之間有必定區別。

  • 緩存一致性

    計算機數據須要通過內存、計算機緩存再到寄存器,計算機緩存一致性是指硬件層面的問題,指的是因爲多核計算機中有多套緩存,各個緩存之間的數據不一致問題。緩存一致性協議(如MESI)就是用來解決多個緩存副本之間的數據一致性問題。

  • 內存一致性

    線程的數據則是放在內存中,共享副本也是,內存一致性保證的是多線程程序併發時的數據一致性問題。咱們常見的volatile、synchronized關鍵字就是用來解決內存一致性問題。這裏屏蔽了計算機硬件問題,主要解決原子性、可見性和有序性問題。

至於內存一致性與緩存一致性問題之間的關係,就是實現內存一致性時須要利用到底層的緩存一致性(以後的volatile關鍵字會涉及)。

4、併發編程的特性

首先咱們要先了解併發編程的三大特性:原子性,可見性,有序性;

1. 原子性

原子性是指一個操做是不可間斷的,即便是多個線程同時執行,該操做也不會被其餘線程所幹擾。

咱們來看一下Java中幾條常見的指令是否具備原子性

  • x = 10

    private int x;
    
    // 具備原子性
    x = 10;
  • i++

    不具有原子性,由於i++包括瞭如下三個步驟:

    1. 讀取 i 的值到內存空間
    2. i + 1
    3. 刷新結果到內存
  • y =x

    private int x, y;
    x = 10;
    
    /*
    y = x沒有原子性
    	1. 把數據x讀到工做空間(這一步具備原子性)
    	2. 把x的值寫到y中(這一步也具備原子性)
    */
    y = x;

總結:多個原子性的操做結合在一塊兒的操做並不具有原子性。

2. 可見性

內存可見性(Memory visibility)是指當某個線程正在使用對象狀態而同時另外一個線程正在修改該狀態,此時須要確保當一個線程修改了對象狀態後,其餘線程可以看到發生的狀態變化。

正如咱們上面所說的,每一個線程都有一個私有的本地工做內存並存儲了線程間讀/寫的共享副本。因此當一個線程對這個副本進行修改而沒有將這個修改後的值寫入主內存中,亦或者這個修改後的值寫入了主內存中而其餘線程並無去訪問主內存中的值,依舊使用的是本地工做內存中的值,那麼此時的併發就有產生問題。咱們來看下面代碼:

public class NoVisibility {

    public static boolean ready = false;

    private static class ReaderThread extends Thread {
        public void run() {
            while (true) {
                if (ready) {
                    System.out.println("=== 即將結束循環 ===");
                    break;
                }
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        new ReaderThread().start();
        Thread.sleep(2000);
        ready = true;
        System.out.println("ready = " + ready);
    }

}

上面代碼極可能會持續循環下去,永遠不會打印出"=== 循環即將結束 ==="這段文字。咱們在代碼中能夠顯而易見的有兩個線程,主線程和咱們聲明的ReaderThread線程。 ready 這個變量在ReaderThread線程開始循環時就已經被複制一份到本地工做內存中了,當主線程修改ready的值爲true時,此修改對於其餘線程並不可見,ReaderThread線程並無去讀取新的值,一直使用本地工做內存中的值,因此會形成無限循環。

對於這個問題很好解決,只要將ready變量聲明爲volatile變量便可。

3. 有序性

有序性即程序按照咱們代碼所書寫的那樣,按其前後順序執行。第一次接觸這個特性可能會有所疑惑,因此在瞭解有序性以前咱們須要來了解執行重排序以及相關概念。

3.1 指令重排序

爲了提升性能,編譯器和處理器會對程序的指令作重排序操做,重排序分爲3種類型:

  1. 編譯器優化的重排序:屬於編譯器重排序,編譯器在不改變單線程程序語義的前提下,能夠從新安排語句的執行順序;
  2. 指令級並行的重排序:屬於處理器重排序,現代處理器採用指令級並行技術來將多條指令重疊執行。若是不存在數據依賴,處理器能夠改變語句對應機器指令的執行順序;
  3. 內存系統的重排序:處於處理器重排序因爲處理器使用緩存和讀/寫緩衝區,這使得加載和存儲操做看上去多是在亂序執行。

指令重排序對於程序執行有利有弊,咱們並非要去徹底禁止它。對於編譯器重排序,JMM的編譯器重排序規則會禁止特定類型的編譯器重排序。對於處理器重排序,JMM的處理器重排序規則會要求Java編譯器在生成指令序列時,插入特定類型個的內存屏障指令,經過內存屏障指令來禁止特定的處理器重排序。

3.2 as-if-serial

as-if-serial語義的意思是:無論怎麼重排序,(單線程)程序的執行結果不能被改變。編譯器、runtime和處理器都必須遵照as-if-serial語義。

爲了遵照as-if-serial語義,編譯器和處理器不會對存在數據依賴關係的操做作重排序。由於這種重排序會改變執行結果。可是,若是操做之間不存在數據依賴關係,這些操做就可能被編譯器和處理器重排序。爲了具體說明,請看下面計算圓面積的代碼示例:

double pi = 3.14;         // A
double r = 1.0;           // B
double area = pi * r * r  // C

如上個圖所示,A和C之間存在數據依賴關係,同時B和C之間也存在數據依賴關係。一次在最終執行的指令序列種,C不能被重排序到A和B前面(C排到A和B的前面,程序的結果將會被改變)。但A和B之間沒有數據依賴關係,編譯器和處理器能夠重排序A和B之間的執行順序。下圖是該程序的兩種執行順序:

as-if-serial語義把單線程程序保護起來,遵照as-if-serial語義的編譯器、runtime和處理器共同爲編寫單線程程序的程序員建立一個幻覺:單線程程序是按程序順序來執行的。as-if-serial語義使單線程程序員無需擔憂重排序會干擾他們,也無需擔憂內存可見性問題。

3.3 happens-before

若是說 as-if-serial 是 JMM 提供用來解決單線程間的內存可見性問題的話,那麼 happens-before 就是JMM向程序員提供的可跨越線程的內存可見性保證。具體表現爲:若是線程A的寫操做a與線程B的讀操做b之間具備 happens-before 關係,那麼JMM將保證這個操做a對操做b可見。此外,happens-before 還有傳遞關係,表現爲:a happens-before b,b happens-before c,那麼a happens-before c。

注意:兩個操做之間存在happens-before關係,並不意味着一個操做必需要在後一個操做以前執行,只要求前一個操做執行的結果對後一個操做可見。若是重排序以後的執行結果,與按happens-before關係來執行的結果一致,那麼這種重排序並不違法(也就是說,JMM容許這種重排序)。

比對 happens-before 與 as-if-serial。

  1. as-if-serial語義保證單線程內程序的執行結果不被改變,happens-before關係保證正確同步的多線程程序的執行結果不被改變。

  2. as-if-serial語義給編寫單線程程序的程序員創造了一個幻境:單線程程序是按程序的順序來執行的。happens-before關係給編寫正確同步的多線程程序的程序員創造了一個幻境:正確同步的多線程程序是按 happens-before 指定的順序來執行的。

  3. as-if-serial 語義和 happens-before 這麼作的目的,都是爲了在不改變程序執行結果的前提下,儘量地提升程序執行的並行度。

因此,總的說來 happens-before 與 as-if-serial 在本質上是同一種概念。

5、volatile變量

volatile能夠視爲輕量級的synchronized,能夠確保共享變量在各個線程間的「可見性」。

1. volatile內存語義

咱們能夠將volatile變量的讀寫操做分別視之爲 get 方法和 set 方法,因此從內存可見性的角度看,寫入volatile變量至關於退出同步代碼塊,而讀取volatile變量就至關於進入同步代碼塊。

2. volatile緩存可見性實現原理

底層實現主要是經過彙編lock前綴指令,它會鎖定這塊內存區域的緩存(緩存行鎖定)並回寫到主內存。其中lock前綴指令在多核處理器下會引起兩件事情:

  1. 會將當前處理器緩存行的數據當即回寫到系統內存。
  2. 這個寫回內存的操做會引發在其餘CPU裏緩存了該內存地址的數據無效(經過MESI緩存一致性協議)。

3. volatile的應用

volatile變量的一個種典型的用法:檢查某個狀態標記以判斷是否退出循環。

還有單例模式的實現,典型的雙重檢查鎖定(即DCL)。

參考

相關文章
相關標籤/搜索