一、JMM的必要性
眾所周知,數(shù)據(jù)競(jìng)爭(zhēng)(Data Racing)在并發(fā)編程中是個(gè)重要問(wèn)題。操作系統(tǒng)的很大一部分任務(wù)就是在協(xié)調(diào)資源的分配,尤其是內(nèi)存資源的分配。例如,線程A和線程B同時(shí)獲取一個(gè)共享內(nèi)存中的int變量,誰(shuí)應(yīng)該優(yōu)先獲取這個(gè)變量呢?從數(shù)據(jù)競(jìng)爭(zhēng)衍生出的一個(gè)新問(wèn)題則是線程間的通信問(wèn)題,即內(nèi)存可見(jiàn)性問(wèn)題。線程間需要通信則是由線程共享處理器產(chǎn)生的,通常線程在Ready、Running、Blocked三個(gè)狀態(tài)中不斷切換,直到線程結(jié)束。


不僅線程狀態(tài)切換可以導(dǎo)致內(nèi)存可見(jiàn)性問(wèn)題。為了提升處理器性能,編譯器在生成可執(zhí)行指令以及處理器在執(zhí)行指令時(shí)會(huì)對(duì)指令進(jìn)行重排序。關(guān)于重排序,請(qǐng)參閱:
重排序改變了程序編寫(xiě)時(shí)應(yīng)有的順序,因此產(chǎn)生了內(nèi)存可見(jiàn)性問(wèn)題。為了解決由線程切換和指令重排序產(chǎn)生的內(nèi)存可見(jiàn)性問(wèn)題,Java語(yǔ)言層面的內(nèi)存模型提供了相應(yīng)的解決方法,即Java內(nèi)存模型(JMM)。
二、JMM的內(nèi)存可見(jiàn)性解決方法
1. 重排序規(guī)則限制
JMM在編譯期間遵循了相關(guān)的指令重排序限制,以保證內(nèi)存對(duì)相關(guān)線程可見(jiàn)。
- 遵守?cái)?shù)據(jù)依賴(lài)性: 在重排序過(guò)程中,編譯器和處理器不會(huì)改變存在數(shù)據(jù)依賴(lài)關(guān)系的兩個(gè)操作的執(zhí)行順序。
數(shù)據(jù)依賴(lài)性:如果兩個(gè)操作訪問(wèn)同一個(gè)變量,且這兩個(gè)操作中有一個(gè)為寫(xiě)操作,此時(shí)這兩個(gè)操作之間就存在數(shù)據(jù)依賴(lài)性。 - 遵從as-if-serial原則: 不管怎么重排序(編譯器和處理器為了提高并行度),(單線程)程序的執(zhí)行結(jié)果不能被改變。編譯器,runtime和處理器都必須遵守as-if-serial語(yǔ)義。
也就是說(shuō),沒(méi)有數(shù)據(jù)依賴(lài)關(guān)系的操作有可能會(huì)被編譯器或處理器重排序。下面是一個(gè)計(jì)算長(zhǎng)方形周長(zhǎng)的例子:
int width = 10; // a
int length = 15; // b
int perimeter = (width + length) * 2; // c
a, b, c的依賴(lài)關(guān)系有:
- a ---> c
- b ---> c
也就是c依賴(lài)于a操作和b操作,但是a操作和b操作不存在依賴(lài)關(guān)系。那么程序執(zhí)行順序有如下可能:
- a ---> b ---> c 按順序執(zhí)行,結(jié)果為50
- b ---> a ---> c 重排序執(zhí)行,結(jié)果為50
從上述結(jié)果可以得知:as-if-serial語(yǔ)義保證了程序的單線程執(zhí)行結(jié)果不會(huì)被改變。而程序員在編寫(xiě)時(shí)并不知道編譯后的操作順序和處理器執(zhí)行操縱的順序,但也不用擔(dān)心重排序會(huì)對(duì)我們想要的結(jié)果產(chǎn)生干擾。
2. 關(guān)鍵字保護(hù)
在JSR133中,JMM分別增強(qiáng)了final, volatile, synchronized這三個(gè)關(guān)鍵字的內(nèi)存語(yǔ)義。在編譯期和處理器運(yùn)行指令時(shí),有這三個(gè)關(guān)鍵字的指令將受到重排序保護(hù),相關(guān)的指令不會(huì)被重排序。一起來(lái)看看JMM是如何實(shí)現(xiàn)這些保護(hù)的。
三、 關(guān)鍵字保護(hù)
1. Volatile
1.1 Volatile語(yǔ)義
當(dāng)一個(gè)共享變量聲明為volatile后,該變量的讀/寫(xiě)將會(huì)很特別。被volatile保護(hù)的變量相當(dāng)于改變量的讀/寫(xiě)操作被鎖保護(hù)起來(lái)了。來(lái)看下面兩段代碼(改自程曉明文章):
class VolatileProtection {
volatile long varOne = 0L; // 使用volatile聲明64位的long型變量
public voiid set(long l) {
varOne = l; // volatile變量的單個(gè)寫(xiě)操作
}
public void increase() {
varOne++; // volatile變量的復(fù)合(多個(gè))讀/寫(xiě)操作
}
public long get(){
return varOne; // volatile變量的單個(gè)讀操作
}
}
假設(shè)有多個(gè)線程分別調(diào)用VolatileProtection中的set,increase和get方法,那么上述程序?qū)⒂泻鸵韵鲁绦蛳嗤男Ч?/p>
class SynchronizedProtection {
long varOne = 0L; // 64位的long型普通變量
public synchronized void set(long l) { // 用鎖同步普通變量的單個(gè)寫(xiě)操作
varOne = l;
}
public void increase() { // 普通方法調(diào)用
long temp = get(); // 調(diào)用已同步的讀方法
temp += 1L; // 普通寫(xiě)操作
set(temp); // 調(diào)用已同步的寫(xiě)方法
}
public synchronized long get() { // 用鎖同步普通變量的單個(gè)讀操作
return varOne;
}
}
鎖的語(yǔ)義決定了get()方法和set()方法的操作具有原子性。同樣,受volatile保護(hù)的變量在讀/寫(xiě)操作上也具有原子性。volatile的特性可以總結(jié)為:
- 可見(jiàn)性:一個(gè)
volatile變量的讀,總是能看到任意線程對(duì)這個(gè)volatile變量最后的寫(xiě)入 - 原子性:
volatile變量的單個(gè)讀/寫(xiě)句有原子性,但類(lèi)似于volatile++這種復(fù)合操作不具原子性。
1.2 Volatile的內(nèi)存語(yǔ)義
我們已經(jīng)知道volatile變量的寫(xiě)/讀具有原子性,那么volatile變量是如何在內(nèi)存中實(shí)現(xiàn)這些語(yǔ)義的呢?來(lái)看看volatile寫(xiě)和讀的內(nèi)存語(yǔ)義。
-
Volatile寫(xiě):當(dāng)我們往共享內(nèi)存中寫(xiě)入一個(gè)volatile變量時(shí),JMM會(huì)把對(duì)應(yīng)線程中的本地內(nèi)存中的貢獻(xiàn)變量值寫(xiě)入主內(nèi)存(即共享內(nèi)存)。 -
Volatile讀:當(dāng)我們讀取一個(gè)volatile變量時(shí),JMM會(huì)把對(duì)應(yīng)線程的本地內(nèi)存中現(xiàn)有的變量重置為無(wú)效,緊接著會(huì)從主內(nèi)存中讀取共享變量值。
1.3 Volatile內(nèi)存語(yǔ)義的實(shí)現(xiàn)
前面說(shuō)到JMM會(huì)在讀volatile變量時(shí)重置本地內(nèi)存,并在寫(xiě)volatile變量時(shí)將線程本地內(nèi)存中的值刷入共享內(nèi)存。在線程不斷切換狀態(tài)讓出處理器的情況下,JMM如何保證這些操作的原子性呢? 這就涉及到JMM實(shí)現(xiàn)volatile讀/寫(xiě)的內(nèi)存語(yǔ)義的方法。
JMM對(duì)編譯器制定了有關(guān)volatile重排序的規(guī)則表:
| 是否能重排序 | 第二個(gè)操作 | ||
|---|---|---|---|
| 第一個(gè)操作 | 普通讀/寫(xiě) | volatile讀 | volatile寫(xiě) |
| 普通讀/寫(xiě) | NO | ||
| volatile讀 | NO | NO | NO |
| volatile寫(xiě) | NO | NO |
由上表我們可以得知,JMM通過(guò)禁止與volatile讀/寫(xiě)相關(guān)的重排序來(lái)保證volatile變量操作的原子性。為了實(shí)現(xiàn)相關(guān)指令的重排序保護(hù),編譯器會(huì)在volatile讀/寫(xiě)操作的指令前后添加相關(guān)屏障(Barrier),因此處理器無(wú)法越過(guò)屏障進(jìn)行重排序。
2. Final
2.1 Final的語(yǔ)義
對(duì)于final域,編譯器和處理器遵循以下兩個(gè)重排序規(guī)則:
- 在構(gòu)造函數(shù)內(nèi)對(duì)一個(gè)final域的寫(xiě)入,與隨后把這個(gè)被構(gòu)造對(duì)象的引用賦值給一個(gè)引用變量,這兩個(gè)操作之間不能重排序。
- 初次讀一個(gè)包含final域的對(duì)象的引用,與隨后初次讀這個(gè)final域,這兩個(gè)操作不能重排序。
public class FinalExample {
int i; // 普通變量
final int j; // final 變量
static FinalExample obj;
public void FinalExample() { // 構(gòu)造函數(shù)
i = 1; // 寫(xiě)普通域 (可能被重排序到構(gòu)造函數(shù)之外)
j = 2; // 寫(xiě)final域 (不會(huì)被重排序到構(gòu)造函數(shù)之外)
}
public static void writer() { // 寫(xiě)線程A執(zhí)行
obj = new FinalExample();
}
public static void reader() { // 讀線程B執(zhí)行
FinalExample object = obj; // 初次讀對(duì)象引用 a
int a = object.i; // 初次讀普通域 b
int b = object.j; // 初次讀final域 c (a與c被禁止重排序)
}
}
2.2 Final域的重排序規(guī)則
-
寫(xiě)Final域:
- JMM禁止編譯器把final域的寫(xiě)重排序到構(gòu)造函數(shù)之外。編譯器通過(guò)在final域的寫(xiě)操作之后,構(gòu)造函數(shù)
return之前,插入一個(gè)StoreStore屏障來(lái)達(dá)到緊致重排序的目的。
- JMM禁止編譯器把final域的寫(xiě)重排序到構(gòu)造函數(shù)之外。編譯器通過(guò)在final域的寫(xiě)操作之后,構(gòu)造函數(shù)
-
讀Final域:
-
在一個(gè)線程中,JMM禁止處理器重排序以下兩個(gè)操作:
- 初次讀對(duì)象引用
- 初次讀該對(duì)象包含的
final域
編譯器通過(guò)在讀
final域操作的前面插入一個(gè)LoadLoad屏障來(lái)實(shí)現(xiàn)禁止重排序。
-
個(gè)人認(rèn)為寫(xiě)final域的重排序規(guī)則比較晦澀,因?yàn)槊總€(gè)構(gòu)造函數(shù)中的操作都應(yīng)該禁止被重排序到構(gòu)造函數(shù)結(jié)束之外。假設(shè)有操作被重排序到構(gòu)造函數(shù)結(jié)束后,那么這個(gè)對(duì)象算是初始化完成了還是未完成呢?按理說(shuō)構(gòu)造函數(shù)完成了,對(duì)象初始化完成;可是構(gòu)造函數(shù)里邊的操作并沒(méi)有結(jié)束,相關(guān)域還沒(méi)被初始化,對(duì)象不能算完成構(gòu)建。所以對(duì)我而言,寫(xiě)Final域不需要重排序,換而言之,構(gòu)造函數(shù)里的所有操作都必須被禁止重排序到構(gòu)造函數(shù)結(jié)束之后。
讀Final域的重排序規(guī)則比較容易理解:因?yàn)槌醮巫x對(duì)象引用的操作a相當(dāng)于初始化FinalExample類(lèi)型的引用變量object,而初次讀object.j的操作c必須要基于object已經(jīng)被初始化了的基礎(chǔ)之上,顯然不能重排序。
2.3 final引用不能從構(gòu)造函數(shù)逸出
- 寫(xiě)Final域的另一個(gè)重排序規(guī)則:
- 在引用變量為任意線程可見(jiàn)之前,該引用變量指向的對(duì)象的
final域已經(jīng)在構(gòu)造函數(shù)中被正確初始化了。也就是不能讓這個(gè)被構(gòu)造對(duì)象的引用為其他線程可見(jiàn)。
- 在引用變量為任意線程可見(jiàn)之前,該引用變量指向的對(duì)象的
四、鎖
除了相關(guān)重排序規(guī)則和關(guān)鍵字保護(hù)以外,Java鎖也提供了內(nèi)存可見(jiàn)性問(wèn)題的解決方法。
鎖可以保證臨界區(qū)內(nèi)的操作具有原子性,從而解決內(nèi)存可見(jiàn)性問(wèn)題。Java的用volatile來(lái)實(shí)對(duì)state的保護(hù),即保證每次獲取鎖和釋放鎖都具有原子操作。
五、總結(jié)
JMM主要通過(guò)禁止相關(guān)指令的重排序來(lái)解決內(nèi)存可見(jiàn)性問(wèn)題。不管是關(guān)鍵字volatile,final,還是鎖,都使用禁止重排序的方法來(lái)實(shí)現(xiàn)相關(guān)功能。