什么是JMM內(nèi)存模型
內(nèi)存模型可以理解為在特定的操作協(xié)議下,對(duì)特定的內(nèi)存或高速緩存進(jìn)行讀寫訪問的過程抽象描述,不同架構(gòu)下的物理機(jī)擁有不一樣的內(nèi)存模型。
JMM(Java內(nèi)存模型)源于CPU架構(gòu)的內(nèi)存模型(用于解決多處理器架構(gòu)系統(tǒng)中的緩存一致性問題)。JVM為了屏蔽各個(gè)硬件平臺(tái)和操作系統(tǒng)對(duì)內(nèi)存訪問機(jī)制的差異化阔墩,提出了JMM概念。因此它不是對(duì)物理內(nèi)存的規(guī)范瓶珊,而是在虛擬機(jī)基礎(chǔ)上進(jìn)行的規(guī)范從而實(shí)現(xiàn)平臺(tái)一致性啸箫。
Java內(nèi)存模型(Java Memory Model,JMM)是一種抽象的概念伞芹,它描述的是一組規(guī)則或規(guī)范筐高,通過這組規(guī)范定義了程序中各個(gè)變量(包括實(shí)例字段、靜態(tài)字段和構(gòu)成數(shù)組對(duì)象的元素)的訪問方式丑瞧。
JMM結(jié)構(gòu)規(guī)范
JMM規(guī)定了所有的變量都存儲(chǔ)在主內(nèi)存(Main Memory)中。每個(gè)線程還有自己的工作內(nèi)存(Working Memory)蜀肘,線程的工作內(nèi)存中保存了該線程使用到的變量的主內(nèi)存的副本拷貝绊汹,線程對(duì)變量的所有操作(讀取、賦值等)都必須在工作內(nèi)存中進(jìn)行扮宠,而不能直接讀寫主內(nèi)存中的變量西乖。不同的線程之間也無法直接訪問對(duì)方工作內(nèi)存中的變量,線程之間值的傳遞都需要通過主內(nèi)存來完成
主內(nèi)存&工作內(nèi)存
主內(nèi)存
主要存儲(chǔ)的是Java實(shí)例對(duì)象坛增,所有線程創(chuàng)建的實(shí)例對(duì)象都存放在主內(nèi)存中(除開開啟了逃逸分析和標(biāo)量替換的棧上分配和TLAB分配)获雕,不管該實(shí)例對(duì)象是成員變量還是方法中的本地變量,也包括共享的類信息收捣、常量届案、靜態(tài)變量。由于是共享數(shù)據(jù)區(qū)域罢艾,多條線程對(duì)同一個(gè)變量進(jìn)行非原子性操作時(shí)可能會(huì)發(fā)生線程安全問題
工作內(nèi)存
主要存儲(chǔ)當(dāng)前方法的所有本地變量信息(工作內(nèi)存中存儲(chǔ)這主內(nèi)存中的變量副本拷貝)楣颠,每個(gè)線程只能訪問自己的工作內(nèi)存尽纽,即線程中的本地變量對(duì)其他線程是不可見的,就算是兩個(gè)線程執(zhí)行的是同一段代碼童漩,它們也會(huì)各自在自己的工作內(nèi)存中創(chuàng)建屬于當(dāng)前線程的本地變量弄贿,當(dāng)然也包括字節(jié)碼行號(hào)指示器、相關(guān)Native方法的信息
交互協(xié)議
八大原子操作
主內(nèi)存與工作內(nèi)存之間的具體交互協(xié)議矫膨,Java內(nèi)存模型定義了八種操作來完成:
-
Lock(鎖定)
作用于主內(nèi)存的變量差凹,把一個(gè)變量標(biāo)記為一條線程獨(dú)占狀態(tài) -
Read(讀取)
作用于主內(nèi)存的變量侧馅,把一個(gè)變量從主內(nèi)存?zhèn)鬏數(shù)骄€程的工作內(nèi)存中 -
Load(加載)
作用于工作內(nèi)存的變量危尿,把Read操作從主內(nèi)存中得到的變量值放入工作內(nèi)存的變量副本中 -
Use(使用)
作用于工作內(nèi)存的變量,把工作內(nèi)存中一個(gè)變量的值傳遞給執(zhí)行引擎施禾,每當(dāng)虛擬機(jī)遇到一個(gè)需要使用變量的值的字節(jié)碼指令時(shí)將會(huì)執(zhí)行這個(gè)操作 -
Assign(賦值)
作用于工作內(nèi)存的變量脚线,把一個(gè)從執(zhí)行引擎接收到的值賦給工作內(nèi)存的變量,每當(dāng)虛擬機(jī)遇到一個(gè)給變量賦值的字節(jié)碼指令時(shí)執(zhí)行這個(gè)操作 -
Store(存儲(chǔ))
作用于工作內(nèi)存的變量弥搞,把工作內(nèi)存中的一個(gè)變量的值傳遞到主內(nèi)存中 -
Write(寫入)
作用于主內(nèi)存的變量邮绿,把Store操作從工作內(nèi)存中得到的變量的值放入主內(nèi)存的變量中 -
Unlock(解鎖)
作用于主內(nèi)存的變量,把一個(gè)處于鎖定狀態(tài)的變量釋放出來攀例,釋放后的變量才可以被其他線程鎖定
同步原則
- 不允許一個(gè)線程無原因地(沒有發(fā)生過assign操作)把數(shù)據(jù)從工作內(nèi)存同步回主內(nèi)存中
- 一個(gè)新的變量只能在主內(nèi)存中誕生船逮,不允許在工作內(nèi)存中直接使用一個(gè)未被初始化(load或assign)的變量。即對(duì)一個(gè)變量實(shí)施use和store之前粤铭,必須先執(zhí)行assign和load操作
- 如果對(duì)一個(gè)變量執(zhí)行l(wèi)ock操作挖胃,將會(huì)清空工作內(nèi)存中此變量的值,在執(zhí)行引擎使用這個(gè)變量之前需要重新執(zhí)行l(wèi)oad或assign操作初始化變量的值
- 如果一個(gè)變量事先沒有被lock操作鎖定梆惯,則不允許對(duì)它執(zhí)行unlock操作酱鸭;也不允許去unlock一個(gè)被其他線程鎖定的變量
- 對(duì)一個(gè)變量執(zhí)行unlock操作之前,必須先把此變量同步到主內(nèi)存中(執(zhí)行store和write操作)
與JVM內(nèi)存結(jié)構(gòu)的區(qū)別
JVM內(nèi)存結(jié)構(gòu)和JMM內(nèi)存模型是完全兩個(gè)不同的概念垛吗。
JVM內(nèi)存結(jié)構(gòu)是處于Java虛擬機(jī)層面的凹髓,是運(yùn)行時(shí)對(duì)Java進(jìn)程占用的內(nèi)存進(jìn)行的一種邏輯上的劃分,通過不同數(shù)據(jù)結(jié)構(gòu)來對(duì)申請(qǐng)的內(nèi)存進(jìn)行不同使用怯屉。對(duì)操作系統(tǒng)來說蔚舀,本質(zhì)上JVM還是存在于主存中
JMM是Java語言與OS和硬件架構(gòu)層面的,本質(zhì)上JMM并不能說是某種技術(shù)實(shí)現(xiàn)锨络,而是一種在多線程并發(fā)情況下對(duì)于共享變量讀寫的規(guī)范赌躺。JMM屏蔽了不同操作系統(tǒng)差異,是跨平臺(tái)可用的內(nèi)存模型羡儿,用來描述線程的數(shù)據(jù)在何時(shí)從主內(nèi)存讀取礼患,何時(shí)寫入主內(nèi)存,解決線程間數(shù)據(jù)共享和傳遞的問題
OS與JVM線程關(guān)系
Java線程的實(shí)現(xiàn)是基于一對(duì)一的線程模型。所謂一對(duì)一模型讶泰,實(shí)際上就是通過語言級(jí)別層面程序去間接調(diào)用系統(tǒng)內(nèi)核的線程模型咏瑟。
我們?cè)谑褂肑ava線程時(shí),如new Thread(Runnable);
痪署,JVM內(nèi)部是調(diào)用當(dāng)前操作系統(tǒng)的內(nèi)核線程來完成當(dāng)前Runnable任務(wù)码泞。我們編寫的多線程程序?qū)儆谡Z言層面的,程序一般不會(huì)直接去調(diào)用內(nèi)核線程狼犯,而是創(chuàng)建一個(gè)應(yīng)用線程映射到一個(gè)內(nèi)核線程余寥,然后通過該線程調(diào)用內(nèi)核線程,進(jìn)而由操作系統(tǒng)內(nèi)核將任務(wù)映射到各個(gè)處理器悯森。這種應(yīng)用線程與內(nèi)核線程間一對(duì)一的關(guān)系就稱為Java程序中的線程與OS的一對(duì)一模型宋舷。
三大特性
JMM是圍繞著并發(fā)編程中原子性、可見性瓢姻、有序性這三個(gè)特征來建立的
原子性
原子性指的是一個(gè)操作是不可中斷的祝蝠,即使是在多線程環(huán)境下,一個(gè)操作一旦開始就不會(huì)被其他線程影響幻碱。
基本類型數(shù)據(jù)的訪問大都是原子操作绎狭,long 和 double 類型的變量是64位的,在32位JVM中褥傍,32位的JVM會(huì)將64位數(shù)據(jù)的讀寫操作分為2次32位的讀寫操作來進(jìn)行儡嘶,這就導(dǎo)致long、double類型的變量在32位虛擬機(jī)中是非原子性操作恍风,數(shù)據(jù)有可能會(huì)被破壞蹦狂,也就意味著多線程在并發(fā)訪問的時(shí)候是線程非安全的
可見性
一個(gè)線程對(duì)共享變量做了修改后,其他的線程立即能夠看到該變量的這種修改
對(duì)于串行程序來說朋贬,可見性是不存在的凯楔,因?yàn)槲覀冊(cè)谌魏我粋€(gè)操作中修改了某個(gè)變量的值,后續(xù)的操作中都能讀取這個(gè)變量值锦募,并且是修改過的新值摆屯。但在多線程環(huán)境中,工作內(nèi)存與主內(nèi)存同步延遲現(xiàn)象就會(huì)造成可見性問題御滩,另外重排序也可能導(dǎo)致可見性問題
有序性
對(duì)于一個(gè)線程的代碼而言,代碼的執(zhí)行總是從前往后依次執(zhí)行的
在單線程環(huán)境下党远,代碼由編碼的順序從上往下執(zhí)行削解,就算發(fā)生指令重排序,由于所有硬件優(yōu)化的前提都是必須遵守 as-if-serial 語義沟娱,所以不管怎么排序氛驮,都不會(huì)且不能影響單線程程序的執(zhí)行結(jié)果。對(duì)于多線程環(huán)境济似,則可能出現(xiàn)亂序現(xiàn)象矫废,因?yàn)槌绦蚓幾g成機(jī)器碼指令后可能會(huì)出現(xiàn)指令重排序現(xiàn)象盏缤,重排后的指令與原指令的順序未必一致(因?yàn)橹噶钪嘏判颥F(xiàn)象以及工作內(nèi)存與主內(nèi)存同步延遲現(xiàn)象導(dǎo)致)。
解決方案
對(duì)于原子性問題蓖扑,JVM提供了對(duì)基本數(shù)據(jù)類型讀寫操作的原子性唉铜,對(duì)于單個(gè)變量(包括64位long和double)可以用volatile關(guān)鍵字來保證讀寫操作的原子性,但volatile關(guān)鍵字對(duì)多個(gè)volatile操作或類似volatile++這種復(fù)合操作不具有原子性律杠;對(duì)于方法級(jí)別或代碼塊級(jí)別的原子性操作潭流,可以使用 synchronized 關(guān)鍵字或Lock鎖來保證程序執(zhí)行的原子性。
對(duì)于工作內(nèi)存與主內(nèi)存同步延遲現(xiàn)象導(dǎo)致的可見性問題柜去,可以使用加鎖或volatile關(guān)鍵字來解決
對(duì)于指令重排序?qū)е碌目梢娦詥栴}和有序性問題灰嫉,可以利用volatile關(guān)鍵字解決
同時(shí),JMM內(nèi)部還定義了一套happens-before原則來保證多線程環(huán)境下兩個(gè)操作間的原子性嗓奢、可見性以及有序性
as-if-serial語義
無論什么語言讼撒,只要操作之間沒有數(shù)據(jù)依賴性,編譯器和CPU都可以任意重排序股耽,因?yàn)閳?zhí)行結(jié)果不會(huì)改變根盒,代碼看起來就像是完全串行地一行行從頭執(zhí)行到尾,這就是as-if-serial語義
對(duì)于單線程來說豺谈,編譯器和CPU可能做了重排序郑象,但開發(fā)者感知不到,也不存在內(nèi)存可見性問題
對(duì)于多線程來說茬末,線程之間的數(shù)據(jù)依賴性太復(fù)雜厂榛,編譯器和CPU沒有辦法完全理解這種依賴性并據(jù)此做出最合理的優(yōu)化。編譯器和CPU只能保證每個(gè)線程的 as-if-serial 語義丽惭,線程之間的數(shù)據(jù)依賴和相互影響击奶,需要上層來確定。
happens-before原則
從JDK5開始责掏,Java使用新的JSR-133內(nèi)存模型柜砾。JSR-133提出了 happens-before 概念,通過這個(gè)概念來闡述操作之間的內(nèi)存可見性换衬。
happens-before 表達(dá)的是:前一個(gè)操作的結(jié)果需要對(duì)后續(xù)操作是可見的痰驱。這里提到的兩個(gè)操作既可以是在一個(gè)線程內(nèi),也可以是不同線程之間瞳浦。
class VolatileExample {
int x = 0;
volatile boolean v = false;
public void writer() {
x = 42;
v = true;
}
public void reader() {
if (v == true) {
//x的值是多少呢担映?
}
}
}
-
程序次序規(guī)則
在一個(gè)線程中,按照代碼的順序叫潦,前面的操作happens-before于后面的任意操作
例如:賦值操作 x = 42 先于 v = true 執(zhí)行 -
volatile變量規(guī)則
對(duì)一個(gè)volatile變量的寫操作happens-before與后續(xù)對(duì)這個(gè)變量的讀操作 -
傳遞規(guī)則
如果 A happens-before B蝇完,并且 B happens-before C,則 A happens-before C
"x=42" happens-before 寫變量 "v=true"。(程序次序規(guī)則)
寫變量"v=42" happens-before 讀變量"v==true"短蜕。(volatile變量規(guī)則)
根據(jù)傳遞性規(guī)則氢架,得到結(jié)果 "x=42" happens-before 讀變量"v==true"
-
監(jiān)視器鎖規(guī)則
對(duì)一個(gè)鎖的解鎖 happens-before 于隨后對(duì)這個(gè)鎖的加鎖
線程A執(zhí)行完代碼后x的值會(huì)變成12,線程B進(jìn)入代碼塊朋魔,能夠看到線程A對(duì)x的寫操作岖研,也就是線程B能夠看到x==12
synchronized (this) { // 此處自動(dòng)加鎖
// x是共享變量,初始值=10
if (this.x < 12) {
this.x = 12;
}
} // 此處自動(dòng)解鎖
-
線程啟動(dòng)規(guī)則
如果線程A調(diào)用線程B的start()方法來啟動(dòng)線程B铺厨,則start()操作happens-before于線程B中的任意操作
線程A啟動(dòng)線程B之后缎玫,線程B能夠看到線程A在啟動(dòng)線程B之前的操作,在線程B中訪問到x變量的值為100
// 在線程A中初始化線程B
Thread threadB = new Thread(() -> {
// 此處的變量x的值是多少呢解滓?答案是100
});
// 線程A在啟動(dòng)線程B之前將共享變量x的值修改為100
x = 100;
// 啟動(dòng)線程B
threadB.start();
-
線程終結(jié)規(guī)則
線程A等待線程B完成(在線程A中調(diào)用線程B的join()方法實(shí)現(xiàn))赃磨,當(dāng)線程B完成后,線程A能夠訪問到線程B對(duì)共享變量的操作
Thread threadB = new Thread(() -> {
// 在線程B中洼裤,將共享變量x的值修改為100
x = 100;
});
// 在線程A中啟動(dòng)線程B
threadB.start();
// 在線程A中等待線程B執(zhí)行完成
threadB.join();
// 此處訪問共享變量x的值為100
-
線程中斷規(guī)則
對(duì)線程interrupt()方法的調(diào)用happens-before于被中斷線程的代碼檢測(cè)到中斷事件的發(fā)生
// 在線程A中將x變量的值初始化為0
private int x = 0;
public void execute() {
// 在線程A中初始化線程B
Thread threadB = new Thread(() -> {
// 線程B檢測(cè)自己是否被中斷
if (Thread.currentThread().isInterrupted()) {
// 如果線程B被中斷邻辉,則此時(shí)x的值為100
System.out.println(x);
}
});
// 在線程A中啟動(dòng)線程B
threadB.start();
// 在線程A中將共享變量x的值修改為100
x = 100;
// 在線程A中中斷線程B
threadB.interrupt();
}
-
對(duì)象終結(jié)規(guī)則
一個(gè)對(duì)象的初始化完成happens-before于它的finalize()方法的開始
public class TestThread {
public TestThread() {
System.out.println("構(gòu)造方法");
}
@Override
public void finalize() throws Throwable {
System.out.println("對(duì)象銷毀");
}
public static void main(String[] args) {
new TestThread();
System.gc();
}
}
運(yùn)行結(jié)果
構(gòu)造方法
對(duì)象銷毀