天天看點

JVM3:現代cpu模型和 JMM底層實作原理一.現代cpu模型在多線程下的不足二.java 記憶體模型JMM

一.現代cpu模型在多線程下的不足

1.寫在前面

一段代碼引來的思考:為什麼程式一直走不出Thread_One的while循環呢?

public class Test{

    public static boolean threadOneFlag = true;

    public volatile static boolean threadTwoFlag = true;
    
    public static void main(String[] args) throws InterruptedException {
        new Thread(()->{
            System.out.println("thread_one_start");
            while (threadOneFlag){ }
            System.out.println("thread_one_end");
        },"Thread_One").start();

        new Thread(()->{
            System.out.println("thread_two_start");
            while (threadTwoFlag){ }
            System.out.println("thread_two_end");
        },"Thread_Two").start();
        Thread.sleep(1000);
        //對threadOneFlag變量的修改線上程Thread_One中并不可見
        threadOneFlag = false;
        threadTwoFlag = false;
    }
}
           

運作結果:

JVM3:現代cpu模型和 JMM底層實作原理一.現代cpu模型在多線程下的不足二.java 記憶體模型JMM

2.從硬體層面了解可見性的本質

程式運作時用到的儲存設備有:CPU、記憶體、磁盤(IO裝置),三者有不同的處理速度,而且差異很大。當一個程式運作時如果三者都需要通路,如果不做任何處理的話,計算效率受限于最慢的裝置,計算機硬體對此做了一些優化:

  • CPU增加了高速緩存
  • 多核CPU并且增加了程序、線程概念,通過時間片切換最大化提升CPU的使用率
  • 編譯器的指令優化,更合理的去利用好CPU的高速緩存

    這些優化雖然提升了計算機的計算效率,但是卻帶來的可見性和重排序的問題,下面慢慢講解

3.CPU高速緩存

  • 存在的意義:絕大多數的運算任務不能僅通過處理器來完成,還需要和記憶體進行互動。例如:讀取運算資料,存儲運算結果。因為計算機的儲存設備與處理器運算速度差距很大,是以會增加CPU高速緩存作為兩者之間的緩沖:将運算需要使用的資料複制到緩存中,讓運算能快速進行,當運算結束後再從緩存同步到記憶體之中。
  • 存在的弊端:會帶來緩存一緻性的問題
  • CPU高速緩存的結構:

    分為L1,L2,L3三級緩存,L1和L2是CPU私有的,其中L1最小,L1又分為資料緩存和指令緩存

    JVM3:現代cpu模型和 JMM底層實作原理一.現代cpu模型在多線程下的不足二.java 記憶體模型JMM

4.緩存一緻性

  • 當高速緩存存在以後,每個CPU擷取/存儲資料直接操作高速緩存,而不是記憶體,這樣當多個線程運作在不同CPU中時。同一份記憶體資料就可能會緩存于多個CPU高速緩存中,如不進行限制,就會出現緩存一緻性問題
  • CPU層面提出了兩種解決辦法:1. 總線鎖,2. 緩存鎖

5.總線鎖和緩存鎖

  • 總線鎖:在多CPU下,當其中一個處理器要對共享記憶體進行操作的時候,在總線上發出一個LOCK信号,使得其他處理器無法通路共享資料,開銷很大,如果我們能夠控制鎖的粒度就能減少開銷,進而引入了緩存鎖。
  • 緩存鎖:隻要保證多個CPU緩存的同一份資料是一緻的就可以了,基于緩存一緻性協定來實作的

6.緩存一緻性協定

為了達到資料通路的一緻,需要各個處理器在通路緩存時遵循一些協定,在讀寫時根據協定來操作,常見的協定有MSI、MESI、MOSI。最常見的是MESI協定。

7.MESI協定

在MESI協定中,每個緩存的緩存控制器不僅知道自己的讀寫操作,而且也監聽其他Cache的讀寫操作。共有四種狀态,分别是:

  • M(Modify)表示共享資料隻緩存在目前CPU緩存中,并且是被修改的狀态。此時表示目前CPU緩存資料與主記憶體中不一緻,其他CPU緩存中如果緩存了目前資料應是無效狀态,因為該資料已被修改且并沒更新到主記憶體
  • E(Exclusive)表示緩存的獨占狀态,資料隻緩存在目前CPU緩存中,并且沒有被修改
  • S(Shared)表示資料可能被多個CPU緩存,并且各個緩存中的資料和主記憶體中的資料一緻
  • I(Invalid)表示目前緩存已經失效
  • 圖解四種狀态:
    JVM3:現代cpu模型和 JMM底層實作原理一.現代cpu模型在多線程下的不足二.java 記憶體模型JMM
    JVM3:現代cpu模型和 JMM底層實作原理一.現代cpu模型在多線程下的不足二.java 記憶體模型JMM
    JVM3:現代cpu模型和 JMM底層實作原理一.現代cpu模型在多線程下的不足二.java 記憶體模型JMM
  • 對于MESI協定,從CPU讀寫角度來說會遵循一下原則:
    1. CPU讀請求:緩存處于M、E、S狀态都可以被讀取,I狀态CPU隻能從主記憶體中讀取資料
    2. CPU寫請求:緩存處于M、E狀态才可以被寫入主記憶體中。對于S狀态的寫,需要将其他CPU中緩存行設定為無效才可寫。
  • 使用總線鎖和緩存鎖機制之後,CPU對于記憶體的操作可以做如下抽象:
    JVM3:現代cpu模型和 JMM底層實作原理一.現代cpu模型在多線程下的不足二.java 記憶體模型JMM

8.MESI協定的不足之處

  • 當一個CPU_0需要将緩存中的資料進行寫入時,首先需要發送失效資訊給其他緩存了該資料的CPU,等回執确認之後才會進行寫入。等待回執确認的過程中CPU_0會處于阻塞狀态,為了避免阻塞造成的資源浪費,CPU中引入了Store Bufferes。
  • 引入Sotr Bufferes後,CPU_0在寫入共享資料時,隻需将資料寫入store bufferes中,同時向其他緩存了共享資料的CPU發送失效指令就可以做其他操作了。由store bufferes等待回執确認資訊,并負責同步到主記憶體
    JVM3:現代cpu模型和 JMM底層實作原理一.現代cpu模型在多線程下的不足二.java 記憶體模型JMM
  • 這種優化方式帶來了兩個現象,引起重排序的問題:
    1. 資料什麼時候送出不确定,因為需要等待其他CPU确認回執之後才會送出,這是一個異步操作
    2. 引入storebufferes後,處理器會先嘗試從storebuffere中讀取值,如果storebufferes中有資料,則直接從storebuffer中讀取,否則再從緩存行中讀取

9.寫合并

現代CPU采用了大量的技術來抵消記憶體通路帶來的延遲。讀寫記憶體資料期間,CPU能執行成百上千條指令。

多級SRAM緩存是減小這種延遲帶來的影響的主要手段。此外,SMP系統采用消息傳遞協定來實作緩存之間的一緻性。遺憾的是,現代的CPU實在是太快了,即使是使用了緩存,

有時也無法跟上CPU  的速度。是以,為了進一步減小延遲的影響,一些鮮為人知的緩沖區派上了用場。

本文将探讨“合并寫存儲緩沖區(write combining store buffers)”,以及如何寫出有效利用它們的代碼。

10.指令重排序

  • 請看如下代碼:假如exeToCPU0和exeToCPU1執行在不同CPU上,當exeToCPU0執行完兩行指派代碼時,此時exeToCPU1執行if語句時,isFinsh = true,但是可能value并不為10,這就是重排序問題。
  • 原因在于:假設CPU0緩存的兩個變量及狀态為:isFinish(E),value(S),CPU0修改value時隻會先将修改結果儲存到Store Buffer中,然後繼續執行isFinish=true指令,因為isFinish是(E),是以會直接将修改結果寫入記憶體中。此時CPU1讀書兩個值時,可能的結果就是:isfinish=true,value=3(不等于10)
    JVM3:現代cpu模型和 JMM底層實作原理一.現代cpu模型在多線程下的不足二.java 記憶體模型JMM
  • 為了解決此類問題,CPU層面提出了記憶體屏障

11.CPU層面的記憶體屏障

  • 可以将其粗犷的了解為:将store buffer中的指令寫入到記憶體,進而使得其他通路同一共享記憶體的線程的可見性
  • X86的 memory barrier的指令包括:讀屏障、寫屏障以及全屏障
  • 寫屏障:告訴處理器在寫屏障之前的所有已經存儲在存儲緩存(store bufferes)中的資料同步到主記憶體,也就是,寫屏障之前的指令對于屏障之後的讀操作都是可見的。
  • 讀屏障:處理器讀屏障之後的讀操作都在屏障之後執行
  • 全屏障:確定屏障前的記憶體讀寫操作的結果都對屏障之後的操作可見
  • 這些都不需要我們程式員來維護,和我們直接打交道的是JMM

由上可看出現代CPU資料一緻性實作=緩存鎖(MESI等各種協定)+總線鎖

并不能解決多線程資料一緻性的問題,是以java要自己實作自己的記憶體模型 Java Memory Model

二.java 記憶體模型JMM

1.什麼是JMM

  • JMM全稱是Java Memory Model,是隸屬于JVM的,是屬于語言級别的抽象記憶體模型,可以簡單了解為對硬體模型的抽象,它定義了共享記憶體中多線程程式讀寫操作的行為規範。JMM并沒有提升或者損失執行性能,也沒有直接限制指令重排序,JMM隻是将底層問題抽象到JVM層面,是基于CPU層面提供的記憶體屏障及限制編譯器的重排序來解決問題的
  • JMM抽象模型分為主記憶體和工作記憶體。主記憶體是所有線程共享的,工作記憶體是每個線程獨占的。線程對變量的所有操作都必須在工作記憶體中進行,不能直接讀寫主記憶體中的變量,線程之間共享變量的傳遞都是基于主記憶體來完成的
  • JMM體統了一些禁用緩存以及禁止重排序的方法,來解決可見性和有序性問題,例如:volatile、synchronized、final
  • 在JMM中如果一個操作的執行結果必須對另外一個操作可見,兩個操作必須要存在happens-before關系,即happen-before規則(具體參見:happen-before規則)。

我們常說的JVM記憶體模式指的是JVM的記憶體分區;而Java記憶體模式是一種虛拟機規範。

Java虛拟機規範中定義了Java記憶體模型(Java Memory Model,JMM),用于屏蔽掉各種硬體和作業系統的記憶體通路差異,以實作讓Java程式在各種平台下都能達到一緻的并發效果,JMM規範了Java虛拟機與計算機記憶體是如何協同工作的:規定了一個線程如何和何時可以看到由其他線程修改過後的共享變量的值,以及在必須時如何同步的通路共享變量。

原始的Java記憶體模型存在一些不足,是以Java記憶體模型在Java1.5時被重新修訂。這個版本的Java記憶體模型在Java8中仍然在使用。

Java記憶體模型(不僅僅是JVM記憶體分區):調用棧和本地變量存放線上程棧上,對象存放在堆上。

JVM3:現代cpu模型和 JMM底層實作原理一.現代cpu模型在多線程下的不足二.java 記憶體模型JMM
JVM3:現代cpu模型和 JMM底層實作原理一.現代cpu模型在多線程下的不足二.java 記憶體模型JMM
  • 一個本地變量可能是原始類型,在這種情況下,它總是“呆在”線程棧上。
  • 一個本地變量也可能是指向一個對象的一個引用。在這種情況下,引用(這個本地變量)存放線上程棧上,但是對象本身存放在堆上。
  • 一個對象可能包含方法,這些方法可能包含本地變量。這些本地變量仍然存放線上程棧上,即使這些方法所屬的對象存放在堆上。
  • 一個對象的成員變量可能随着這個對象自身存放在堆上。不管這個成員變量是原始類型還是引用類型。
  • 靜态成員變量跟随着類定義一起也存放在堆上。
  • 存放在堆上的對象可以被所有持有對這個對象引用的線程通路。當一個線程可以通路一個對象時,它也可以通路這個對象的成員變量。如果兩個線程同時調用同一個對象上的同一個方法,它們将會都通路這個對象的成員變量,但是每一個線程都擁有這個成員變量的私有拷貝。

Java記憶體模型和硬體記憶體架構之間的橋接

Java記憶體模型與硬體記憶體架構之間存在差異。硬體記憶體架構沒有區分線程棧和堆。對于硬體,所有的線程棧和堆都分布在主記憶體中。部分線程棧和堆可能有時候會出現在CPU緩存中和CPU内部的寄存器中。如下圖所示:

JVM3:現代cpu模型和 JMM底層實作原理一.現代cpu模型在多線程下的不足二.java 記憶體模型JMM

從抽象的角度來看,JMM定義了線程和主記憶體之間的抽象關系:

  • 線程之間的共享變量存儲在主記憶體(Main Memory)中
  • 每個線程都有一個私有的本地記憶體(Local Memory),本地記憶體是JMM的一個抽象概念,并不真實存在,它涵蓋了緩存、寫緩沖區、寄存器以及其他的硬體和編譯器優化。本地記憶體中存儲了該線程以讀/寫共享變量的拷貝副本。
  • 從更低的層次來說,主記憶體就是硬體的記憶體,而為了擷取更好的運作速度,虛拟機及硬體系統可能會讓工作記憶體優先存儲于寄存器和高速緩存中。
  • Java記憶體模型中的線程的工作記憶體(working memory)是cpu的寄存器和高速緩存的抽象描述。而JVM的靜态記憶體儲模型(JVM記憶體模型)隻是一種對記憶體的實體劃分而已,它隻局限在記憶體,而且隻局限在JVM的記憶體。
JVM3:現代cpu模型和 JMM底層實作原理一.現代cpu模型在多線程下的不足二.java 記憶體模型JMM
JVM3:現代cpu模型和 JMM底層實作原理一.現代cpu模型在多線程下的不足二.java 記憶體模型JMM

JMM模型下的線程間通信:

線程間通信必須要經過主記憶體。

如下,如果線程A與線程B之間要通信的話,必須要經曆下面2個步驟:

1)線程A把本地記憶體A中更新過的共享變量重新整理到主記憶體中去。

2)線程B到主記憶體中去讀取線程A之前已更新過的共享變量。

JVM3:現代cpu模型和 JMM底層實作原理一.現代cpu模型在多線程下的不足二.java 記憶體模型JMM

關于主記憶體與工作記憶體之間的具體互動協定,即一個變量如何從主記憶體拷貝到工作記憶體、如何從工作記憶體同步到主記憶體之間的實作細節,

Java記憶體模型定義了以下八種操作來完成,簡稱8大原子操作(在最新的JSR-133中已經棄用,了解即可):

  • lock(鎖定):作用于主記憶體的變量,把一個變量辨別為一條線程獨占狀态。
  • unlock(解鎖):作用于主記憶體變量,把一個處于鎖定狀态的變量釋放出來,釋放後的變量才可以被其他線程鎖定。
  • read(讀取):作用于主記憶體變量,把一個變量值從主記憶體傳輸到線程的工作記憶體中,以便随後的load動作使用
  • load(載入):作用于工作記憶體的變量,它把read操作從主記憶體中得到的變量值放入工作記憶體的變量副本中。
  • use(使用):作用于工作記憶體的變量,把工作記憶體中的一個變量值傳遞給執行引擎,每當虛拟機遇到一個需要使用變量的值的位元組碼指令時将會執行這個操作。
  • assign(指派):作用于工作記憶體的變量,它把一個從執行引擎接收到的值指派給工作記憶體的變量,每當虛拟機遇到一個給變量指派的位元組碼指令時執行這個操作。
  • store(存儲):作用于工作記憶體的變量,把工作記憶體中的一個變量的值傳送到主記憶體中,以便随後的write的操作。
  • write(寫入):作用于主記憶體的變量,它把store操作從工作記憶體中一個變量的值傳送到主記憶體的變量中。

Java記憶體模型還規定了在執行上述八種基本操作時,必須滿足如下規則:

  • 如果要把一個變量從主記憶體中複制到工作記憶體,就需要按順尋地執行read和load操作, 如果把變量從工作記憶體中同步回主記憶體中,就要按順序地執行store和write操作。但Java記憶體模型隻要求上述操作必須按順序執行,而沒有保證必須是連續執行。
  • 不允許read和load、store和write操作之一單獨出現
  • 不允許一個線程丢棄它的最近assign的操作,即變量在工作記憶體中改變了之後必須同步到主記憶體中。
  • 不允許一個線程無原因地(沒有發生過任何assign操作)把資料從工作記憶體同步回主記憶體中。
  • 一個新的變量隻能在主記憶體中誕生,不允許在工作記憶體中直接使用一個未被初始化(load或assign)的變量。即就是對一個變量實施use和store操作之前,必須先執行過了assign和load操作。
  • 一個變量在同一時刻隻允許一條線程對其進行lock操作,但lock操作可以被同一條線程重複執行多次,多次執行lock後,隻有執行相同次數的unlock操作,變量才會被解鎖。lock和unlock必須成對出現
  • 如果對一個變量執行lock操作,将會清空工作記憶體中此變量的值,在執行引擎使用這個變量前需要重新執行load或assign操作初始化變量的值
  • 如果一個變量事先沒有被lock操作鎖定,則不允許對它執行unlock操作;也不允許去unlock一個被其他線程鎖定的變量。
  • 對一個變量執行unlock操作之前,必須先把此變量同步到主記憶體中(執行store和write操作)。

Java記憶體模型解決的問題

當對象和變量被存放在計算機中各種不同的記憶體區域中時,就可能會出現一些具體的問題。Java記憶體模型建立所圍繞的問題:在多線程并發過程中,如何處理多線程讀同步問題與可見性(多線程緩存與指令重排序)、多線程寫同步問題與原子性(多線程競争race condition)。

1、多線程讀同步與可見性

可見性(共享對象可見性):線程對共享變量修改的可見性。當一個線程修改了共享變量的值,其他線程能夠立刻得知這個修改

線程緩存導緻的可見性問題:

如果兩個或者更多的線程在沒有正确的使用volatile聲明或者同步的情況下共享一個對象,一個線程更新這個共享對象可能對其它線程來說是不可見的:共享對象被初始化在主存中。跑在CPU上的一個線程将這個共享對象讀到CPU緩存中,然後修改了這個對象。隻要CPU緩存沒有被重新整理會主存,對象修改後的版本對跑在其它CPU上的線程都是不可見的。這種方式可能導緻每個線程擁有這個共享對象的私有拷貝,每個拷貝停留在不同的CPU緩存中。

下圖示意了這種情形。跑在左邊CPU的線程拷貝這個共享對象到它的CPU緩存中,然後将count變量的值修改為2。這個修改對跑在右邊CPU上的其它線程是不可見的,因為修改後的count的值還沒有被重新整理回主存中去。

JVM3:現代cpu模型和 JMM底層實作原理一.現代cpu模型在多線程下的不足二.java 記憶體模型JMM

解決這個記憶體可見性問題你可以使用:

  • Java中的volatile關鍵字:volatile關鍵字可以保證直接從主存中讀取一個變量,如果這個變量被修改後,總是會被寫回到主存中去。Java記憶體模型是通過在變量修改後将新值同步回主記憶體,在變量讀取前從主記憶體重新整理變量值這種依賴主記憶體作為傳遞媒介的方式來實作可見性的,無論是普通變量還是volatile變量都是如此,普通變量與volatile變量的差別是:volatile的特殊規則保證了新值能立即同步到主記憶體,以及每個線程在每次使用volatile變量前都立即從主記憶體重新整理。是以我們可以說volatile保證了多線程操作時變量的可見性,而普通變量則不能保證這一點。
  • Java中的synchronized關鍵字:同步快的可見性是由“如果對一個變量執行lock操作,将會清空工作記憶體中此變量的值,在執行引擎使用這個變量前需要重新執行load或assign操作初始化變量的值”、“對一個變量執行unlock操作之前,必須先把此變量同步回主記憶體中(執行store和write操作)”這兩條規則獲得的。
  • Java中的final關鍵字:final關鍵字的可見性是指,被final修飾的字段在構造器中一旦被初始化完成,并且構造器沒有把“this”的引用傳遞出去(this引用逃逸是一件很危險的事情,其他線程有可能通過這個引用通路到“初始化了一半”的對象),那麼在其他線程就能看見final字段的值(無須同步)

重排序導緻的可見性問題:

Java程式中天然的有序性可以總結為一句話:如果在本地線程内觀察,所有操作都是有序的(“線程内表現為串行”(Within-Thread As-If-Serial Semantics));如果在一個線程中觀察另一個線程,所有操作都是無序的(“指令重排序”現象和“線程工作記憶體與主記憶體同步延遲”現象)。

Java語言提供了volatile和synchronized兩個關鍵字來保證線程之間操作的有序性:

  • volatile關鍵字本身就包含了禁止指令重排序的語義
  • synchronized則是由“一個變量在同一個時刻隻允許一條線程對其進行lock操作”這條規則獲得的,這個規則決定了持有同一個鎖的兩個同步塊隻能串行地進入

指令序列的重排序:

1)編譯器優化的重排序。編譯器在不改變單線程程式語義的前提下,可以重新安排語句的執行順序。

2)指令級并行的重排序。現代處理器采用了指令級并行技術(Instruction-LevelParallelism,ILP)來将多條指令重疊執行。如果不存在資料依賴性,處理器可以改變語句對應機器指令的執行順序。

3)記憶體系統的重排序。由于處理器使用緩存和讀/寫緩沖區,這使得加載和存儲操作看上去可能是在亂序執行。

JVM3:現代cpu模型和 JMM底層實作原理一.現代cpu模型在多線程下的不足二.java 記憶體模型JMM

每個處理器上的寫緩沖區,僅僅對它所在的處理器可見。這會導緻處理器執行記憶體操作的順序可能會與記憶體實際的操作執行順序不一緻。由于現代的處理器都會使用寫緩沖區,是以現代的處理器都會允許對寫-讀操作進行重排序:

JVM3:現代cpu模型和 JMM底層實作原理一.現代cpu模型在多線程下的不足二.java 記憶體模型JMM

資料依賴:

編譯器和處理器在重排序時,會遵守資料依賴性,編譯器和處理器不會改變存在資料依賴關系的兩個操作的執行順序。(這裡所說的資料依賴性僅針對單個處理器中執行的指令序列和單個線程中執行的操作,不同處理器之間和不同線程之間的資料依賴性不被編譯器和處理器考慮)

JVM3:現代cpu模型和 JMM底層實作原理一.現代cpu模型在多線程下的不足二.java 記憶體模型JMM

指令重排序對記憶體可見性的影響:

JVM3:現代cpu模型和 JMM底層實作原理一.現代cpu模型在多線程下的不足二.java 記憶體模型JMM

當1和2之間沒有資料依賴關系時,1和2之間就可能被重排序(3和4類似)。這樣的結果就是:讀線程B執行4時,不一定能看到寫線程A在執行1時對共享變量的修改。

指令重排序改變多線程程式的執行結果例子:

JVM3:現代cpu模型和 JMM底層實作原理一.現代cpu模型在多線程下的不足二.java 記憶體模型JMM

flag變量是個标記,用來辨別變量a是否已被寫入。這裡假設有兩個線程A和B,A首先執行writer()方法,随後B線程接着執行reader()方法。線程B在執行操作4時,能否看到線程A在操作1對共享變量a的寫入呢?

答案是:不一定能看到。

由于操作1和操作2沒有資料依賴關系,編譯器和處理器可以對這兩個操作重排序;同樣,操作3和操作4沒有資料依賴關系,編譯器和處理器也可以對這兩個操作重排序。

as-if-serial語義:

不管怎麼重排序(編譯器和處理器為了提高并行度),(單線程)程式的執行結果不能被改變。(編譯器、runtime和處理器都必須遵守as-if-serial語義)

happens before:

從JDK 5開始,Java使用新的JSR-133記憶體模型,JSR-133使用happens-before的概念來闡述操作之間的記憶體可見性:在JMM中,如果一個操作執行的結果需要對另一個操作可見(兩個操作既可以是在一個線程之内,也可以是在不同線程之間),那麼這兩個操作之間必須要存在happens-before關系:

  • 程式順序規則:一個線程中的每個操作,happens-before于該線程中的任意後續操作。
  • 螢幕鎖規則:對一個鎖的解鎖,happens-before于随後對這個鎖的加鎖。
  • volatile變量規則:對一個volatile域的寫,happens-before于任意後續對這個volatile域的讀。
  • 傳遞性:如果A happens-before B,且B happens-before C,那麼A happens-before C。

一個happens-before規則對應于一個或多個編譯器和處理器重排序規則

記憶體屏障禁止特定類型的處理器重排序:

重排序可能會導緻多線程程式出現記憶體可見性問題。對于處理器重排序,JMM的處理器重排序規則會要求Java編譯器在生成指令序列時,插入特定類型的記憶體屏障(Memory Barriers,Intel稱之為Memory Fence)指令,通過記憶體屏障指令來禁止特定類型的處理器重排序。通過禁止特定類型的編譯器重排序和處理器重排序,為程式員提供一緻的記憶體可見性保證。

為了保證記憶體可見性,Java編譯器在生成指令序列的适當位置會插入記憶體屏障指令來禁止特定類型的處理器重排序。

JVM3:現代cpu模型和 JMM底層實作原理一.現代cpu模型在多線程下的不足二.java 記憶體模型JMM

StoreLoad Barriers是一個“全能型”的屏障,它同時具有其他3個屏障的效果。現代的多處理器大多支援該屏障(其他類型的屏障不一定被所有處理器支援)。執行該屏障開銷會很昂貴,因為目前處理器通常要把寫緩沖區中的資料全部重新整理到記憶體中(Buffer Fully Flush)。

2、多線程寫同步與原子性

多線程競争(Race Conditions)問題:當讀,寫和檢查共享變量時出現race conditions。

如果兩個或者更多的線程共享一個對象,多個線程在這個共享對象上更新變量,就有可能發生race conditions。

想象一下,如果線程A讀一個共享對象的變量count到它的CPU緩存中。再想象一下,線程B也做了同樣的事情,但是往一個不同的CPU緩存中。現線上程A将count加1,線程B也做了同樣的事情。現在count已經被增加了兩次,每個CPU緩存中一次。如果這些增加操作被順序的執行,變量count應該被增加兩次,然後原值+2被寫回到主存中去。然而,兩次增加都是在沒有适當的同步下并發執行的。無論是線程A還是線程B将count修改後的版本寫回到主存中取,修改後的值僅會被原值大1,盡管增加了兩次:

JVM3:現代cpu模型和 JMM底層實作原理一.現代cpu模型在多線程下的不足二.java 記憶體模型JMM

解決這個問題可以使用Java同步塊。一個同步塊可以保證在同一時刻僅有一個線程可以進入代碼的臨界區。同步塊還可以保證代碼塊中所有被通路的變量将會從主存中讀入,當線程退出同步代碼塊時,所有被更新的變量都會被重新整理回主存中去,不管這個變量是否被聲明為volatile。

使用原子性保證多線程寫同步問題:

原子性:指一個操作是按原子的方式執行的。要麼該操作不被執行;要麼以原子方式執行,即執行過程中不會被其它線程中斷。

  • Reads and writes are atomic for reference variables and for most primitive variables (all types except long and double).
  • Reads and writes are atomic for all variables declared volatile (including long and double variables).

(https://docs.oracle.com/javase/tutorial/essential/concurrency/atomic.html)

實作原子性:

  • 由Java記憶體模型來直接保證的原子性變量操作包括read、load、assign、use、store、write,我們大緻可以認為基本資料類型變量、引用類型變量、聲明為volatile的任何類型變量的通路讀寫是具備原子性的(long和double的非原子性協定:對于64位的資料,如long和double,Java記憶體模型規範允許虛拟機将沒有被volatile修飾的64位資料的讀寫操作劃分為兩次32位的操作來進行,即允許虛拟機實作選擇可以不保證64位資料類型的load、store、read和write這四個操作的原子性,即如果有多個線程共享一個并未聲明為volatile的long或double類型的變量,并且同時對它們進行讀取和修改操作,那麼某些線程可能會讀取到一個既非原值,也不是其他線程修改值的代表了“半個變量”的數值。但由于目前各種平台下的商用虛拟機幾乎都選擇把64位資料的讀寫操作作為原子操作來對待,是以在編寫代碼時一般也不需要将用到的long和double變量專門聲明為volatile)。這些類型變量的讀、寫天然具有原子性,但類似于 “基本變量++” / “volatile++” 這種複合操作并沒有原子性。
  • 如果應用場景需要一個更大範圍的原子性保證,需要使用同步塊技術。Java記憶體模型提供了lock和unlock操作來滿足這種需求。虛拟機提供了位元組碼指令monitorenter和monitorexist來隐式地使用這兩個操作,這兩個位元組碼指令反映到Java代碼中就是同步快——synchronized關鍵字。

JMM對特殊Java語義的特殊規則支援

volatile總結 (保證記憶體可見性:Lock字首的指令、記憶體屏障禁止重排序)

synchronized總結 (保證記憶體可見性和操作原子性:互斥鎖;鎖優化)

參考來源:

《Java并發程式設計的藝術》

《深入了解Java記憶體模型》

《深入了解Java虛拟機》

http://ifeve.com/java-memory-model-6/

http://tutorials.jenkov.com/java-concurrency/java-memory-model.html

(本文首發于微信公衆号:EnjoyMoving)

https://wx4.sinaimg.cn/mw690/73036ef6ly1fwn6kdgxm8j20c00cfmye.jpg(公衆号二維碼)

繼續閱讀