什么是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)

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)存的變量中

重排序
通常我們假設(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ā)生)法則
- 程序次序規(guī)則:一個(gè)線程內(nèi),按照代碼順序,書寫在前面的操作先行發(fā)生于書寫在后面的操作;
- 鎖定規(guī)則:一個(gè)unLock操作先行發(fā)生于后面對(duì)同一個(gè)鎖的lock操作;
- volatile變量規(guī)則:對(duì)一個(gè)變量的寫操作先行發(fā)生于后面對(duì)這個(gè)變量的讀操作;
- 傳遞規(guī)則:如果操作A先行發(fā)生于操作B,而操作B又先行發(fā)生于操作C,則可以得出操作A先行發(fā)生于操作C;
- 線程啟動(dòng)規(guī)則:Thread對(duì)象的start()方法先行發(fā)生于此線程的每個(gè)一個(gè)動(dòng)作;
- 線程中斷規(guī)則:對(duì)線程interrupt()方法的調(diào)用先行發(fā)生于被中斷線程的代碼檢測(cè)到中斷事件的發(fā)生;
- 線程終結(jié)規(guī)則:線程中所有的操作都先行發(fā)生于線程的終止檢測(cè),我們可以通過Thread.join()方法結(jié)束、Thread.isAlive()的返回值手段檢測(cè)到線程已經(jīng)終止執(zhí)行;
- 對(duì)象終結(jié)規(guī)則:一個(gè)對(duì)象的初始化完成先行發(fā)生于他的finalize()方法的開始;
happens-before的前后兩個(gè)操作不會(huì)被重排序且后者對(duì)前者的內(nèi)存可見
as-if-serial語義和happens-before法則可以概括成:
- 在單線程環(huán)境下不能改變程序運(yùn)行的結(jié)果
- 存在數(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/0在x=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法則
參考資料