天天看點

【Java并發程式設計】Java記憶體模型Java記憶體模型

Java記憶體模型

一、JMM解析

之前寫過一篇文章

【Java核心技術卷】談談對Java平台的了解

,其中讨論“Java跨平台”的篇幅占了大半的位置,JVM的重要性不言而喻。

為了能夠屏蔽各種硬體以及對作業系統的記憶體通路的差異,而且要能使得Java程式在各個平台下都能達到一緻的并發效果。JVM規範中定義了Java的記憶體模型(Java Memory model, JMM)。

JMM是一種規範,它規範了JVM與計算機記憶體是如何協同工作的,規定了一個線程如何以及何時看到其他線程修改過的共享變量的值以及在必須時如何同步地通路到共享變量。

【Java并發程式設計】Java記憶體模型Java記憶體模型

下面我們來認識認識JMM,首先看一下規範下的JVM的記憶體配置設定。Heap為堆,Stack 為棧。

堆是一個運作時的資料區,也是Java的垃圾回收器重點關注的對象。堆的優勢在于可以動态配置設定記憶體的大小,缺點是因為是運作時動态配置設定記憶體,存取速度要慢一點。

棧的優勢存取的速度比堆要快,但是比計算機的寄存器要慢哈,棧的資料是可以共享的,但是存在棧中的資料的大小與生存期是确定的,靈活性較低,棧中主要存放一些基本類型的變量。比如int,short,long,byte,對象句柄等。

JMM要求調用棧和本地變量存放線上程棧上,對象存放在堆上。對了,一個對象可能包含方法,方法可能包含本地變量,這些本地變量仍然是存放線上程棧上的,即使這些對象擁有這些方法,也是要把這些對象放在堆中。

一個對象的成員變量随着對象存放在堆中,無論這些成員變量是原始類型還是引用類型。靜态成員變量跟随類的定義一起存放在堆上,存放在堆上的對象可以被持有這個對象的引用的線程通路。線程通過引用通路這個對象的時候,也是能夠通路這個對象的成員變量。如果多個線程同時調用同一個對象同一個方法,它們可能都将通路這個對象的成員變量,這個時候每個線程都會擁有相應的成員變量的

私有拷貝

【Java并發程式設計】Java記憶體模型Java記憶體模型

私有拷貝,有疑惑嗎?我這裡示範一下吧

public class Main {
    
    public static void main(String[] args) {
        new Thread(() -> {
            Person person1 = new Person();
            person1.count();
            System.out.println(person1.getId());
        }).start();

        new Thread(() -> {
            Person person2 = new Person();
            person2.count();
            System.out.println(person2.getId());
        }).start();
    }
}

class Person {
    private Long id = 0L;
    public void count(){
        id++;
    }

    public Long getId() {
        return id;
    }
}           

結果:

【Java并發程式設計】Java記憶體模型Java記憶體模型

上面是在JVM層次看多線程的。

下面看看硬體記憶體架構

二、硬體記憶體架構

【Java并發程式設計】Java記憶體模型Java記憶體模型

上面展示的是多個CPU,有個概念,這裡需要點一下,你可千萬别搞混了

多核CPU指的是一個CPU有多個CPU核心,多核CPU性能非常好,但成本較高;如果沒錢,可以換為多個單核的CPU,如果有錢可以換成多個多核的CPU。

現在我們使用的計算機大都都是多個多核CPU了,這使得在實際使用的時候,會有多個CPU上都跑的有程序(線程),我們的Java程式如果是并發的,可能會在多個CPU上跑。

我們看一下CPU的寄存器,每個CPU都包含一系列的寄存器,它們是CPU記憶體的基礎,寄存器執行的速度遠大于主存上執行的速度,中間的緩存,我就不多說了,上篇文章介紹過了

【Java并發程式設計】CPU多級緩存

CPU從主存中讀取資料的時候,首先會将資料讀取到緩存中,然後由緩存讀取到寄存器中,然後再去執行,執行完步驟後,如果需要将結果寫回到主存中,首先要将資料重新整理到緩存中,緩存會在未來的某個時間點,将結果重新整理到主存中。

三、JMM與硬體記憶體架構的關聯

【Java并發程式設計】Java記憶體模型Java記憶體模型

Java記憶體模型與硬體架構模型之間是存在一些差異的,硬體架構模型沒有區分線程棧與堆。對于硬體而言,所有的線程棧與堆都配置設定在主記憶體裡面,部分線程棧和堆可能會出現在CPU的緩存中和CPU内部的寄存器中。

四、Java線程與計算機主記憶體之間的抽象關系

線程之間的共享變量存儲在主記憶體裡面,每一個線程都有一個私有的本地記憶體,本地記憶體是Java記憶體模型抽象的概念,并不是真實存在的,它涵蓋了緩存、寄存器以及其他的硬體和編譯器的優化等,本地記憶體中存儲了該線程已讀或寫,共享變量拷貝的一個副本。Java記憶體模型的

工作記憶體

是CPU的寄存器和高速緩存的一個抽象的描述。Java記憶體模型的存儲劃分僅是是對其内部的實體劃分而已,隻局限在JVM的記憶體。

【Java并發程式設計】Java記憶體模型Java記憶體模型

由于每個線程都有自己的本地記憶體,它們如果同時通路主記憶體的共享變量,共享記憶體的值會分别copy到每個線程的本地變量中。每個線程對自己本地記憶體中的值做出的修改對其他線程都是不可見的,這個時候就會導緻不一緻性。

比如說主記憶體某個共享變量值為1,A和B線程都要對這個這個共享變量做出修改,A和B線程都先把值copy到自己的本地記憶體中,然後進行操作,A線程對其進行加1,并将值重新整理到主記憶體中,B線程将其加2,但是相對于A線程慢了半拍,但是也成功将值重新整理到主記憶體中。

此時,主記憶體中這個共享變量的值是3,當A再次從主記憶體中讀取這個共享變量(中間會copy到它的本地記憶體),值已經不是2了。這個時候就導緻了線程的安全性問題。

五、Java記憶體模型中同步八種操作

  1. lock(鎖定):作用于主記憶體的變量,把—個變量辨別為一條線程獨占狀态
  2. unlock(解鎖):作用于主記憶體的變量,把一個處于鎖定狀态的變量釋放出來,釋放後的變量才可以被其他線程鎖定
  3. read(讀取):作用于主記憶體的變量,把一個變量值從主記憶體傳輸到線程的工作記憶體中,以便随後的load動作使用
  4. load(載入):作用于工作記憶體的變量,它把read操作從主記憶體中得到的變量值放入工作記憶體的變量副本中
  5. use(使用):作用于工作記憶體的變量,把工作記憶體中的一個變量值傳遞給執行引擎
  6. assign(指派):作用于工作記憶體的變量,它把一個從執行引擎接收到的值指派給工作內存的變量
  7. store(存儲):作用于工作記憶體的變量,把工作記憶體中的一個變量的值傳送到主記憶體中,以便随後的 write的操作
  8. write(寫入):作用于主記憶體的變量,它把 store操作從工作記憶體中一個變量的值傳送到主記憶體的變量中
【Java并發程式設計】Java記憶體模型Java記憶體模型

Lock作用于主記憶體的變量,它把一個變量辨別為一個線程獨占的狀态,與其對應的就是unlock

Read讀取,也是作用于主記憶體的變量,它變量的值從主記憶體變量輸送到工作記憶體中(未到工作記憶體),與後邊的load動作對接

Load是載入的意思,它将Read操作中變量的值放入工作記憶體的變量副本中

Use是使用,作用于工作記憶體中的變量, 它将工作記憶體中的變量傳遞給執行引擎,每當JVM遇到一個需要使用到的變量值的位元組碼指令的時候就會執行use這個操作。

Assign為指派,作用于工作記憶體中的變量,它把從執行引擎接收到的值指派給工作記憶體中的變量,每當JVM遇到一個需要給變量指派的位元組碼指令的時候就會執行assign這個操作。

接下來是Store,也就是存儲,它作用于工作記憶體中的變量,它将工作記憶體中的變量傳遞到主記憶體中(未到主記憶體),與後邊的write操作對接

Write是寫入的操作,它将Store操作中變量的值,放入到主記憶體的變量裡面。

對應的同步規則有:

  • 如果要把一個變量從主記憶體中複制到工作記憶體,就需要按順尋地執行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—個被其他線程鎖定的變量
  • 對一個變量執行uηlock操作之前,必須先把此變量同步到主記憶體中(執行 store和 write操作)

Java并發相關的類設計時都遵循的規則,還有一些特殊的規則,之後再說。