Java內(nèi)存模型學(xué)習(xí)筆記

什么是JMM

JMM(java內(nèi)存模型)源于物理機(jī)器CPU架構(gòu)的內(nèi)存模型,最初用于解決MP(多處理器架構(gòu))系統(tǒng)中的緩存一致性問題

JMM可以分為工作內(nèi)存和主內(nèi)存

JMM規(guī)定了所有的變量(此處變量特指實(shí)例變量,靜態(tài)變量等,但不包括局部變量和函數(shù)參數(shù),因?yàn)檫@兩種是線程私有)都存儲(chǔ)在主內(nèi)存中,此處的主內(nèi)存僅僅是虛擬機(jī)內(nèi)存的一部分,而虛擬機(jī)內(nèi)存也僅僅是計(jì)算機(jī)物理內(nèi)存的一部分(為虛擬機(jī)進(jìn)程分配的那一部分)
每條線程還有自己的工作內(nèi)存,線程的工作內(nèi)存中保存了被該線程使用到的變量的主內(nèi)存副本拷貝。線程對(duì)變量的所有操作(讀取、賦值),都必須在工作內(nèi)存中進(jìn)行,而不能直接讀寫主內(nèi)存中的變量,這是造成線程安全的主要原因。不同線程之間也無法直接訪問對(duì)方工作內(nèi)存中的變量,線程間變量值的傳遞均需要通過主內(nèi)存來完成,線程、主內(nèi)存、工作內(nèi)存三者之間的交互關(guān)系如下圖

物理內(nèi)存架構(gòu)

內(nèi)存架構(gòu)

Java內(nèi)存模型和硬件內(nèi)存體系結(jié)構(gòu)是不同的。 硬件內(nèi)存體系結(jié)構(gòu)不區(qū)分線程堆棧和堆。 在硬件上,線程堆棧和堆都位于主內(nèi)存中。 部分線程堆棧和堆有時(shí)可能存在于CPU高速緩存和內(nèi)部CPU寄存器中

JMM和JVM內(nèi)存模型的關(guān)系

關(guān)系不大,是兩個(gè)概念,JMM用于解決MP(多處理器架構(gòu))系統(tǒng)中的緩存一致性問題,
而JVM為了屏蔽各個(gè)硬件平臺(tái)和操作系統(tǒng)對(duì)內(nèi)存訪問機(jī)制的差異化,提出了JMM的概念。

在java內(nèi)存模型中,有方法區(qū),堆等概念,比如只要放實(shí)例對(duì)象的地方就叫堆
而在JMM中,所有變量都放在主內(nèi)存中
他們都是虛構(gòu)的,都對(duì)應(yīng)了物理內(nèi)存架構(gòu)中的某一部分

內(nèi)存間交互操作

  • 鎖定(lock):作用于主內(nèi)存變量,把一個(gè)變量標(biāo)識(shí)為一條線程獨(dú)占狀態(tài)。
  • 解鎖(unlock):作用于主內(nèi)存變量,把一個(gè)處于鎖定狀態(tài)的變量釋放出來,釋放后的變量才可以被其他線程鎖定。
  • 讀取(read):一個(gè)變量值從主內(nèi)存?zhèn)鬏數(shù)骄€程的工作內(nèi)存中
  • 載入(load):將從主內(nèi)存得到的變量值放到工作內(nèi)存的變量副本中
  • 使用(use):把工作內(nèi)存中的一個(gè)變量值傳遞給執(zhí)行引擎,每當(dāng)虛擬機(jī)遇到一個(gè)需要使用變量的值的字節(jié)碼指令時(shí)將會(huì)執(zhí)行這個(gè)操作
  • 賦值(assign):它把一個(gè)從執(zhí)行引擎接收到的值賦值給工作內(nèi)存的變量,每當(dāng)虛擬機(jī)遇到一個(gè)給變量賦值的字節(jié)碼指令時(shí)執(zhí)行這個(gè)操作
  • 存儲(chǔ)(store):作用于工作內(nèi)存的變量,把工作內(nèi)存中的一個(gè)變量的值傳送到主內(nèi)存中,以便隨后的write的操作
  • 寫入(write):作用于主內(nèi)存的變量,它把store操作從工作內(nèi)存中一個(gè)變量的值傳送到主內(nèi)存的變量中
交互關(guān)系

重排序

通常我們假設(shè)程序的執(zhí)行是按照編碼順序依次執(zhí)行,這種模型被稱作順序一致性模型,但是現(xiàn)代多處理器架構(gòu)沒有使用這個(gè)模型,而是引入了重排序的概念

什么是重排序?

重排序通常是編譯器或運(yùn)行時(shí)環(huán)境為了優(yōu)化程序性能而采取的對(duì)指令進(jìn)行重新排序執(zhí)行的一種手段。

重排序分為兩類:編譯期重排序和運(yùn)行期重排序(包括指令級(jí)并行的重排序和內(nèi)存系統(tǒng)重排序),分別對(duì)應(yīng)編譯時(shí)和運(yùn)行時(shí)環(huán)境。

主要規(guī)則

能否重排序 第二個(gè)操作
第一個(gè)操作 Normal Load,Normal Store Volatile load,MonitorEnter Volatile store,MonitorExit
Normal Load,Normal Store yes yes no
Volatile load,MonitorEnter no no no
Volatile store,MonitorExit yes no no
  • Normal Load指令包括:對(duì)非volatile字段的讀取,getfield,getstatic和array load;
  • Normal Store指令包括:對(duì)非volatile字段的存儲(chǔ),putfield,putstatic和array store;
  • Volatile load指令包括:對(duì)多線程環(huán)境的volatile變量的讀取,getfield,getstatic;
  • Volatile store指令包括:對(duì)多線程環(huán)境的volatile變量的存儲(chǔ),putfield,putstatic;
  • MonitorEnters指令(包括進(jìn)入同步塊synchronized方法)是用于多線程環(huán)境的鎖對(duì)象;
  • MonitorExits指令(包括離開同步塊synchronized方法)是用于多線程環(huán)境的鎖對(duì)象

編譯期重排序

編譯期重排序的典型就是通過調(diào)整指令順序,在不改變程序語義的前提下,盡可能減少寄存器的讀取、存儲(chǔ)次數(shù),充分復(fù)用寄存器的存儲(chǔ)值

運(yùn)行時(shí)重排序

現(xiàn)代CPU幾乎都采用流水線機(jī)制加快指令的處理速度,一般來說,一條指令需要若干個(gè)CPU時(shí)鐘周期處理,而通過流水線并行執(zhí)行,可以在同等的時(shí)鐘周期內(nèi)執(zhí)行若干條指令,具體做法簡(jiǎn)單地說就是把指令分為不同的執(zhí)行周期,例如讀取、尋址、解析、執(zhí)行等步驟,并放在不同的元件中處理,同時(shí)在執(zhí)行單元EU中,功能單元被分為不同的元件,例如加法元件、乘法元件、加載元件、存儲(chǔ)元件等,可以進(jìn)一步實(shí)現(xiàn)不同的計(jì)算并行執(zhí)行。

as-if-serial語義

as-if-serial語義的意思是,所有的操作均可以為了優(yōu)化而被重排序,但是你必須要保證重排序后執(zhí)行的結(jié)果不能被改變,編譯器、runtime、處理器都必須遵守as-if-serial語義。注意as-if-serial只保證單線程環(huán)境,多線程環(huán)境下無效。

happens-before(先行發(fā)生)法則

  1. 程序次序規(guī)則:一個(gè)線程內(nèi),按照代碼順序,書寫在前面的操作先行發(fā)生于書寫在后面的操作;
  2. 鎖定規(guī)則:一個(gè)unLock操作先行發(fā)生于后面對(duì)同一個(gè)鎖的lock操作;
  3. volatile變量規(guī)則:對(duì)一個(gè)變量的寫操作先行發(fā)生于后面對(duì)這個(gè)變量的讀操作;
  4. 傳遞規(guī)則:如果操作A先行發(fā)生于操作B,而操作B又先行發(fā)生于操作C,則可以得出操作A先行發(fā)生于操作C;
  5. 線程啟動(dòng)規(guī)則:Thread對(duì)象的start()方法先行發(fā)生于此線程的每個(gè)一個(gè)動(dòng)作;
  6. 線程中斷規(guī)則:對(duì)線程interrupt()方法的調(diào)用先行發(fā)生于被中斷線程的代碼檢測(cè)到中斷事件的發(fā)生;
  7. 線程終結(jié)規(guī)則:線程中所有的操作都先行發(fā)生于線程的終止檢測(cè),我們可以通過Thread.join()方法結(jié)束、Thread.isAlive()的返回值手段檢測(cè)到線程已經(jīng)終止執(zhí)行;
  8. 對(duì)象終結(jié)規(guī)則:一個(gè)對(duì)象的初始化完成先行發(fā)生于他的finalize()方法的開始;

happens-before的前后兩個(gè)操作不會(huì)被重排序且后者對(duì)前者的內(nèi)存可見

as-if-serial語義和happens-before法則可以概括成:

  1. 在單線程環(huán)境下不能改變程序運(yùn)行的結(jié)果
  2. 存在數(shù)據(jù)依賴關(guān)系的不允許重排序

可見性問題和有序性問題

可見性問題是對(duì)變量做了修改,而另一個(gè)線程不知道

有序性問題是代碼執(zhí)行順序發(fā)生改變導(dǎo)致獲取到的值異常,變量的值可能并沒有做修改操作

重排序和多線程

一個(gè)例子


public class RecordExample2 {
    
    public void writer(){
         int x, y;
        x = 1;
        try {
            x = 2;
            y = 0 / 0;    
        } catch (Exception e) {
        } finally {
            System.out.println("x = " + x);
        }
    }
}

該例子在多線程中可能不會(huì)輸出預(yù)想的結(jié)果

代碼可能被重排序成了,0/0x=2之前執(zhí)行

多線程可見性問題

一個(gè)經(jīng)典的例子


public class RecordExample2 {
    int a = 0;
    boolean flag = false;

    /**
     * A線程執(zhí)行
     */
    public void writer(){
        a = 1;                  // 1
        flag = true;            // 2
    }

    /**
     * B線程執(zhí)行
     */
    public void read(){
        if(flag){                  // 3
           int i = a + a;          // 4
        }
    }

}

線程B不一定可以看到flag的值被修改了,因?yàn)椴粷M足happens-before法則

參考資料

JVM的重排序

jmm-cookbook

【死磕Java并發(fā)】-----Java內(nèi)存模型之重排序

happens-before俗解

Java內(nèi)存訪問重排序的研究

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容