深入理解JVM第十二章筆記
背景
為了充分壓榨計算機(jī)處理器的性能,多任務(wù)處理在現(xiàn)代計算機(jī)操作系統(tǒng)中已經(jīng)是一項必備技能了。
另外由于大部分的計算任務(wù)都不可能只靠處理器來單獨“計算”完成,處理器需要與內(nèi)存交互,如讀取運(yùn)算數(shù)據(jù),存儲運(yùn)算結(jié)果等,這個IO操作是很難消除的。而如今的計算機(jī)的存儲設(shè)備與處理器的運(yùn)算速度有幾個數(shù)量級的差距,所以需要在處理器與內(nèi)存之間加入一層---高速緩存,用來將運(yùn)算需要使用到的數(shù)據(jù)復(fù)制到緩存中,讓運(yùn)算能快速進(jìn)行,當(dāng)運(yùn)算結(jié)束后再從緩存同步回內(nèi)存之中,這樣處理器就可以無需等待緩慢的內(nèi)存讀寫了。
高速緩存的引入也帶來了一個新的問題:緩存一致性:
多處理器系統(tǒng),每個處理器都有自己的高速緩存,它們又共享同一個主存:

當(dāng)多個處理器的運(yùn)算任務(wù)都涉及同一塊主內(nèi)存區(qū)域時,將可能導(dǎo)致各自的緩存數(shù)據(jù)不一致
為了解決一致性問題,需要各個處理器訪問緩存時都遵循一些協(xié)議,在讀寫時根據(jù)協(xié)議來進(jìn)行操作,這類協(xié)議有:MSI,MESI等等
所謂的“內(nèi)存模型”,可以理解為在特定的操作協(xié)議下,對特定的內(nèi)存或高速緩存進(jìn)行讀寫訪問的過程抽象,不同的物理機(jī)器有不一樣的內(nèi)存模型,JVM也有屬于自己的內(nèi)存模型。
Java內(nèi)存模型
Java虛擬機(jī)規(guī)范定義了一種Java內(nèi)存模型(JMM)來屏蔽各種硬件和操作系統(tǒng)的內(nèi)存訪問差異,以實現(xiàn)讓Java程序在各種平臺下都能達(dá)到一致的內(nèi)存訪問效果。
主內(nèi)存與工作內(nèi)存
JMM的主要目標(biāo)是定義程序中各個變量的訪問規(guī)則,即在虛擬機(jī)中將變量存儲到內(nèi)存和從內(nèi)存中取出變量這樣的底層細(xì)節(jié),此處的變量與Java編程中所說的變量有所區(qū)別,它包括:
實例字段
靜態(tài)字段
構(gòu)成數(shù)組的元素
但不包括局部變量與方法參數(shù),因為它們屬于線程私有的。
JMM規(guī)定了所有變量都存儲在主內(nèi)存中,每條線程還有自己的工作內(nèi)存,線程的工作內(nèi)存中保存了被該線程使用到的變量的主內(nèi)存副本拷貝,線性對變量的所有操作(讀取,賦值等)都必須在工作內(nèi)存進(jìn)行,不能直接讀寫主內(nèi)存中的變量
線程,主內(nèi)存,工作內(nèi)存三者的交互關(guān)系:

內(nèi)存間交互操作
對于主內(nèi)存與工作內(nèi)存之間具體的交互協(xié)議,即一個變量如何從主內(nèi)存拷貝到工作內(nèi)存,如何從工作內(nèi)存同步回主內(nèi)存之類的實現(xiàn)細(xì)節(jié),Java內(nèi)存模型定義了8種操作來完成主內(nèi)存與工作內(nèi)存的讀寫交互,虛擬機(jī)實現(xiàn)保證每一種操作都是原子的,不可再分的
- Lock鎖定 作用于主內(nèi)存變量,將變量標(biāo)志為一條線程所獨占
- Unlock解鎖 作用于主內(nèi)存變量,將處于鎖定的變量釋放出來
- Read讀取 作用于主內(nèi)存變量,它將一個變量的值從主內(nèi)存?zhèn)鬏數(shù)骄€程的工作內(nèi)存中
- Load載入 作用于工作內(nèi)存變量,它把從主內(nèi)存讀取的變量值放入工作內(nèi)存的副本中
- Use使用 作用于工作內(nèi)存變量,將工作內(nèi)存變量值傳遞給執(zhí)行引擎
- Assgin賦值 作用于工作內(nèi)存變量,將執(zhí)行引擎的值傳遞給工作內(nèi)存的變量
- Store存儲 作用于工作內(nèi)存變量,它把工作內(nèi)存變量傳遞到主內(nèi)存中
- Write寫入 作用于主內(nèi)存變量,把Store操作從工作內(nèi)存得到的變量值放入主內(nèi)存變量中
如果要把一個變量從主內(nèi)存復(fù)制到工作內(nèi)存,那就要順序的執(zhí)行read和load操作
如果要把變量從工作內(nèi)存同步回主內(nèi)存,就要順序執(zhí)行store和write操作
Java內(nèi)存模型這2個操作必須順序執(zhí)行,但不保證連續(xù)執(zhí)行,即在指令之間可以插入其它指令
但是Java內(nèi)存模型規(guī)定了一些必要的規(guī)則
- 不允許read load store write單獨出現(xiàn),即不允許一個變量讀取到工作內(nèi)存,但沒有變量接收的情況
- 不允許一個線程丟棄它的assign操作,即變量在工作內(nèi)存改變必須同步回主內(nèi)存
- 不允許一個線程無原因(沒有發(fā)生assgin賦值操作)把數(shù)據(jù)從線程的工作內(nèi)存同步會主內(nèi)存
- 一個新的變量只能在主內(nèi)存中誕生,不允許在工作內(nèi)存中直接使用未被初始化的變量
- 一個變量同一時刻只允許一條線程對其進(jìn)行Lock鎖定,但Lock操作可以被同一線程重復(fù)執(zhí)行
- 如果對一個變量執(zhí)行Lock鎖定,會清空工作內(nèi)存中該副本的值,即執(zhí)行引擎使用該值會重新load assgin操作初始化該值
如果一個變量事先沒有被Lock鎖定,那就不允許進(jìn)行Unlock操作,也不允許Unlock其它線程鎖定的變量
對一個變量執(zhí)行Unlock操作,必須先把此變量值同步回主內(nèi)存(store write操作)
對于volatile型變量的特殊規(guī)則
當(dāng)一個變量定義為volatile之后,它將具備兩種特性:
保證此變量對所有線程的可見性,這里的“可見性”是指當(dāng)一條線程修改了這個變量的值,新值對于其他線程來說是可以立即得知的,而普通變量不能做到這一點。普通變量的值在線程間的傳遞需要通過主存來完成:線程A修改一個普通變量的值,然后向主內(nèi)存進(jìn)行回寫,另外一條線程B在線程A回寫完成之后再從主內(nèi)存進(jìn)行讀取操作,新變量值才會對線程B可見
但是volatile 不能保證線程是安全的,因為java里面的運(yùn)算并非原子操作禁止指令重排序優(yōu)化
原子性 可見性 有序性
- 原子性
大致可以認(rèn)為基本數(shù)據(jù)類型的訪問讀寫是具備原子性的
- 可見性
當(dāng)一個線程修改了共享變量的值,其他線程能夠立即得知這個修改
除了volatile,能夠保證可見性的還有:
synchronized
final
- 有序性
Java提供了volatile和synchronized保證有序性
先行發(fā)生原則
先行發(fā)生是指JMM中定義的兩項操作直接的偏序關(guān)系,如果說操作A先行發(fā)生于操作B,其實就是說在發(fā)生操作B之前,操作A產(chǎn)生的影響能被操作B觀察到,“影響”包括了:
修改了內(nèi)存中共享變量的值
發(fā)送了消息
- 調(diào)用了方法
等等
例子:
i = 1 // 線程A中執(zhí)行
j = i // 線程B執(zhí)行
i = 2 // 線程C執(zhí)行
假設(shè)A中操作" i = 1"先行發(fā)生于B的操作" j = i",那么可以確定在B的操作執(zhí)行后,變量j的值一定等于1,得出這個結(jié)論的依據(jù):
根據(jù)先行發(fā)生原則,“ i=1”的結(jié)果可以被觀察到
C還沒考慮進(jìn)來,A操作結(jié)束之后沒有線程會修改i的值了
現(xiàn)在把C考慮進(jìn)去:
依舊保持A和B之間的先行發(fā)生關(guān)系,C出現(xiàn)在A和B之間,但C和B沒有先行發(fā)生關(guān)系,那j的值就會出現(xiàn)不確定的情況,1或2都有可能:因為C對變量i的影響可能會被B觀察到,也可能不會
下面是JMM中一些“天然的”先行發(fā)生關(guān)系:
- 程序次序規(guī)則:
在一個線程內(nèi),按照程序代碼順序,書寫在前面的操作先行發(fā)生于書寫在后面的操作。準(zhǔn)確地說是控制流順序而不是程序代碼順序,因為要考慮分支、循環(huán)等結(jié)構(gòu)。
- 管程鎖定規(guī)則
一個unlock操作先行發(fā)生于后面對同一個鎖的lock操作。這里必須強(qiáng)調(diào)的是同一個鎖,這里的“后面”指的是時間上的先后順序。
- volatile變量規(guī)則
對一個volatile變量的寫操作先行發(fā)生于后面對這個變量的讀操作,這里的“后面”指的是時間上的先后順序
- 線程啟動規(guī)則
Thread對象的start()方法先行發(fā)生于此線程的每一個動作。
- 線程終止規(guī)則
線程中的所有操作都先行發(fā)生于對此線程的終止檢測,我們可以通過Thread.join()方法結(jié)束、Thread.isAlive()的返回值等手段檢測到線程已經(jīng)終止執(zhí)行。
- 線程中斷規(guī)則
對線程interrupt()方法的調(diào)用先行發(fā)生于被中斷線程的代碼檢測到中斷事件的發(fā)生(即先中斷,后發(fā)現(xiàn)被中斷),可以通過Thread.interrupted()方法檢測到是否有中斷發(fā)生。
- 對象終結(jié)規(guī)則
一個對象的初始化完成(構(gòu)造函數(shù)執(zhí)行結(jié)束)先行發(fā)生于它的finalize()方法的開始。
- 傳遞性
若操作A先行發(fā)生于操作B,操作B先行發(fā)生于操作C,那么就可以得出操作A先行發(fā)生于操作C。
以上規(guī)則是Java語言“天然”存在的規(guī)則,無需同步手段(例:加synchronized)就能保證先行發(fā)生。
例子:
private int value = 0;
public int getValue() {
return value;
}
public void setValue(int value) {
this.value = value;
}
假設(shè):
有線程A和B,A先調(diào)用了setValue(1),然后B調(diào)用了getValue()
那B收到的返回值是什么?
依次分析先行發(fā)生原則中的各項規(guī)則:
- 兩個方法分別由兩個線程調(diào)用,不在一個線程中,所以程序次序規(guī)則在這里不適用
沒有同步塊,自然不會發(fā)生lock和unlock操作,管程鎖定規(guī)則不適用
value變量沒有volatile修飾,volatile變量規(guī)則不適用
后續(xù)的線程啟動,中止,中斷規(guī)則和對象終結(jié)規(guī)則也和這里無關(guān)系
沒有一個適用的先行發(fā)生原則,傳遞性也無從談起,所以這里的操作不是線程安全的
Java與線程
線程的實現(xiàn)
線程的引入可以把一個進(jìn)程的資源分配和執(zhí)行調(diào)度分開,線程既可共享進(jìn)程資源(內(nèi)存地址、文件I/O等),也可獨立調(diào)度(線程是CPU調(diào)度的基本單位)
實現(xiàn)線程有三種方式:
- 1 使用內(nèi)核線程實現(xiàn)

程由內(nèi)核來完成線程切換,內(nèi)核通過操縱調(diào)度器(Scheduler)對線程進(jìn)行調(diào)度,并負(fù)責(zé)將線程的任務(wù)映射到各個處理器上。 每個內(nèi)核線程可以視為內(nèi)核的一個分身,這樣操作系統(tǒng)就有能力同時處理多件事情,支持多線程的內(nèi)核就叫做多線程內(nèi)核(MultiThreads Kernel)。
程序一般不會直接去使用內(nèi)核線程,而是去使用內(nèi)核線程的一種高級接口——輕量級進(jìn)程(Light Weight Process,LWP),輕量級進(jìn)程就是我們通常意義上所講的線程,由于每個輕量級進(jìn)程都由一個內(nèi)核線程支持,因此只有先支持內(nèi)核線程,才能有輕量級進(jìn)程。 這種輕量級進(jìn)程與內(nèi)核線程之間1:1的關(guān)系稱為一對一的線程模型,如下圖所示:
由于內(nèi)核線程的支持,每個輕量級進(jìn)程都成為一個獨立的調(diào)度單元,即使有一個輕量級進(jìn)程在系統(tǒng)調(diào)用中阻塞了,也不會影響整個進(jìn)程繼續(xù)工作,但是輕量級進(jìn)程具有它的局限性:首先,由于是基于內(nèi)核線程實現(xiàn)的,所以各種線程操作,如創(chuàng)建、 析構(gòu)及同步,都需要進(jìn)行系統(tǒng)調(diào)用。 而系統(tǒng)調(diào)用的代價相對較高,需要在用戶態(tài)(User Mode)和內(nèi)核態(tài)(Kernel Mode)中來回切換。 其次,每個輕量級進(jìn)程都需要有一個內(nèi)核線程的支持,因此輕量級進(jìn)程要消耗一定的內(nèi)核資源(如內(nèi)核線程的??臻g),因此一個系統(tǒng)支持輕量級進(jìn)程的數(shù)量是有限的。
- 2 使用用戶線程實現(xiàn)
從廣義上來講,一個線程只要不是內(nèi)核線程,就可以認(rèn)為是用戶線程(UserThread,UT),因此,從這個定義上來講,輕量級進(jìn)程也屬于用戶線程,但輕量級進(jìn)程的實現(xiàn)始終是建立在內(nèi)核之上的,許多操作都要進(jìn)行系統(tǒng)調(diào)用,效率會受到限制。
而狹義上的用戶線程指的是完全建立在用戶空間的線程庫上,系統(tǒng)內(nèi)核不能感知線程存在的實現(xiàn)。 用戶線程的建立、 同步、 銷毀和調(diào)度完全在用戶態(tài)中完成,不需要內(nèi)核的幫助。如果程序?qū)崿F(xiàn)得當(dāng),這種線程不需要切換到內(nèi)核態(tài),因此操作可以是非??焖偾业拖牡模部梢灾С忠?guī)模更大的線程數(shù)量,部分高性能數(shù)據(jù)庫中的多線程就是由用戶線程實現(xiàn)的。 這種進(jìn)程與用戶線程之間1:N的關(guān)系稱為一對多的線程模型,如下圖所示:

使用用戶線程的優(yōu)勢在于不需要系統(tǒng)內(nèi)核支援,劣勢也在于沒有系統(tǒng)內(nèi)核的支援,所有的線程操作都需要用戶程序自己處理。 線程的創(chuàng)建、 切換和調(diào)度都是需要考慮的問題,而且由于操作系統(tǒng)只把處理器資源分配到進(jìn)程,那諸如“阻塞如何處理”、 “多處理器系統(tǒng)中如何將線程映射到其他處理器上”這類問題解決起來將會異常困難,甚至不可能完成。 因而使用用戶線程實現(xiàn)的程序一般都比較復(fù)雜,除了以前在不支持多線程的操作系統(tǒng)中(如DOS)的多線程程序與少數(shù)有特殊需求的程序外,現(xiàn)在使用用戶線程的程序越來越少了,Java、Ruby等語言都曾經(jīng)使用過用戶線程,最終又都放棄使用它。
- 3 使用用戶線程加輕量級進(jìn)程混合實現(xiàn)
線程除了依賴內(nèi)核線程實現(xiàn)和完全由用戶程序自己實現(xiàn)之外,還有一種將內(nèi)核線程與用戶線程一起使用的實現(xiàn)方式。 在這種混合實現(xiàn)下,既存在用戶線程,也存在輕量級進(jìn)程。 用戶線程還是完全建立在用戶空間中,因此用戶線程的創(chuàng)建、 切換、 析構(gòu)等操作依然廉價,并且可以支持大規(guī)模的用戶線程并發(fā)。 而操作系統(tǒng)提供支持的輕量級進(jìn)程則作為用戶線程和內(nèi)核線程之間的橋梁,這樣可以使用內(nèi)核提供的線程調(diào)度功能及處理器映射,并且用戶線程的系統(tǒng)調(diào)用要通過輕量級線程來完成,大大降低了整個進(jìn)程被完全阻塞的風(fēng)險。 在這種混合模式中,用戶線程與輕量級進(jìn)程的數(shù)量比是不定的,即為N:M的關(guān)系,如下圖所示,這種

參考資料
<<深入理解Java虛擬機(jī)>>