定義
Java 虛擬機(jī)規(guī)范中試圖定義一種 Java 內(nèi)存模型來屏蔽掉各種硬件和操作系統(tǒng)的內(nèi)存訪問差異,以實(shí)現(xiàn)讓 Java 程序在各種平臺(tái)下都能達(dá)到一致的內(nèi)存訪問效果。在此之前,主流程序語言(如 C/C++等)直接使用物理硬件和操作系統(tǒng)的內(nèi)存模型,因此,會(huì)由于不同平臺(tái)上內(nèi)存模型的差異,有可能導(dǎo)致程序在一套平臺(tái)上并發(fā)完全正常,而在另外一套平臺(tái)上并發(fā)訪問卻經(jīng)常出錯(cuò),因此在某些場(chǎng)景就必須針對(duì)不同的平臺(tái)來編寫程序。
主內(nèi)存與工作內(nèi)存
Java 內(nèi)存模型的主要目標(biāo)是定義程序中各個(gè)變量的訪問規(guī)則,即在虛擬機(jī)中將變量存儲(chǔ)到內(nèi)存和從內(nèi)存中取出變量這樣的底層細(xì)節(jié)。此處的變量(Variables)與 Java 編程中所說的變量有所區(qū)別,它包括了實(shí)例字段、靜態(tài)字段和構(gòu)成數(shù)組對(duì)象的元素,但不包括局部變量與方法參數(shù),因?yàn)楹笳呤蔷€程私有的,不會(huì)被共享,自然就不會(huì)存在競(jìng)爭(zhēng)問題。
Java 內(nèi)存模型規(guī)定了所有的變量都存儲(chǔ)在主內(nèi)存(Main Memory)中。每條線程還有自己的工作內(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)存間交互操作
Java 內(nèi)存模型定義了 8 種操作來完成工作內(nèi)存與主內(nèi)存之間的交互:一個(gè)變量從主內(nèi)存拷貝到工作內(nèi)存、從工作內(nèi)存同步回主內(nèi)存。虛擬機(jī)實(shí)現(xiàn)時(shí)必須保證下面提及的每一種操作都是原子的、不可再分的。
- lock(鎖定):作用于主內(nèi)存的變量,它把一個(gè)變量標(biāo)識(shí)為一條線程獨(dú)占的狀態(tài)。
- unlock(解鎖):作用于主內(nèi)存的變量,它把一個(gè)處于鎖定狀態(tài)的變量釋放出來,釋放后的變量才可以被其他線程鎖定。
- read(讀取):作用于主內(nèi)存的變量,它把一個(gè)變量的值從主內(nèi)存?zhèn)鬏數(shù)骄€程的工作內(nèi)存中,以便隨后的 load 動(dòng)作使用。
- 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 操作使用。
- write(寫入):作用于主內(nèi)存的變量,它把 store 操作從工作內(nèi)存中得到的變量的值放入主內(nèi)存的變量中。
內(nèi)存模型三大特性
原子性
除了 long 和 double 之外的基本數(shù)據(jù)類型的訪問讀寫是具備原子性的。
Java 內(nèi)存模型允許虛擬機(jī)將沒有被 volatile 修飾的 64 位數(shù)據(jù)的讀寫操作劃分為兩次 32 位的操作來進(jìn)行,即虛擬機(jī)可以不保證 64 位數(shù)據(jù)類型的 load、store、read 和 write 這 4 個(gè)操作的原子性。但是目前各種平臺(tái)下的商用虛擬機(jī)幾乎都選擇把 64 位數(shù)據(jù)的讀寫操作作為原子操作來對(duì)待。
AtomicInteger、AtomicLong、AtomicReference 等特殊的原子性變量類提供了下面形式的原子性條件更新語句,使得比較和更新這兩個(gè)操作能夠不可分割地執(zhí)行。
boolean compareAndSet(expectedValue, updateValue);
AtomicInteger 使用舉例:
private AtomicInteger ai = new AtomicInteger(0); public int next() { return ai.addAndGet(2) }
如果應(yīng)用場(chǎng)景需要一個(gè)更大范圍的原子性保證,Java 內(nèi)存模型還提供了 lock 和 unlock 操作來滿足這種需求,盡管虛擬機(jī)未把 lock 和 unlock 操作直接開放給用戶使用,但是卻提供了更高層次的字節(jié)碼指令 monitorenter 和 monitorexit 來隱式地使用這兩個(gè)操作,這兩個(gè)字節(jié)碼指令反映到 Java 代碼中就是同步塊——synchronized 關(guān)鍵字,因此在 synchronized 塊之間的操作也具備原子性。
可見性
可見性是指當(dāng)一個(gè)線程修改了共享變量的值,其他線程能立即得知這個(gè)修改。
Java 內(nèi)存模型是通過在變量修改后將新值同步回主內(nèi)存,在變量讀取前從主內(nèi)存刷新變量值這種依賴主內(nèi)存作為傳遞媒介的方式來實(shí)現(xiàn)可見性的,無論是普通變量還是 volatile 變量都是如此,普通變量與 volatile 變量的區(qū)別是,volatile 的特殊規(guī)則保證了新值能立即同步到主內(nèi)存,以及每次使用前立即從主內(nèi)存刷新。因此,可以說 volatile 保證了多線程操作時(shí)變量的可見性,而普通變量則不能保證這一點(diǎn)。
除了 volatile 之外,Java 還有兩個(gè)關(guān)鍵字能實(shí)現(xiàn)可見性,即 synchronized 和 final。同步塊的可見性是由“對(duì)一個(gè)變量執(zhí)行 unlock 操作之前,必須先把此變量同步回主內(nèi)存中(執(zhí)行 store、write 操作)”這條規(guī)則獲得的,而 final 關(guān)鍵字的可見性是指:被 final 修飾的字段在構(gòu)造器中一旦初始化完成,并且構(gòu)造器沒有把“this”的引用傳遞出去(this 引用逃逸是一件很危險(xiǎn)的事情,其他線程有可能通過這個(gè)引用訪問到“初始化了一半”的對(duì)象),那在其他線程中就能看見 final 字段的值。
有序性
本線程內(nèi)觀察,所有的操作都是有序的;如果在一個(gè)線程中觀察另一個(gè)線程,所有的操作都是無序的。前半句是指線程內(nèi)表現(xiàn)為串行的語義,后半句是指指令重排和工作內(nèi)存和主內(nèi)存存在同步延遲的現(xiàn)象。
Java 語言提供了 volatile 和 synchronized 兩個(gè)關(guān)鍵字來保證線程之間操作的有序性,volatile 關(guān)鍵字本身就包含了禁止指令重排序的語義,而 synchronized 則是由“一個(gè)變量在同一個(gè)時(shí)刻只允許一條線程對(duì)其進(jìn)行 lock 操作”這條規(guī)則獲得的,這條規(guī)則決定了持有同一個(gè)鎖的兩個(gè)同步塊只能串行地進(jìn)入。
synchronized 關(guān)鍵字在需要這 3 種特性的時(shí)候都可以作為其中一種的解決方案,看起來很“萬能”。的確,大部分的并發(fā)控制操作都能使用 synchronized 來完成。synchronized 的“萬能”也間接造就了它被程序員濫用的局面,越“萬能”的并發(fā)控制,通常會(huì)伴隨著越大的性能影響。
先行發(fā)生原則
如果 Java 內(nèi)存模型中所有的有序性都只靠 volatile 和 synchronized 來完成,那么有一些操作將會(huì)變得很繁瑣,但是我們?cè)诰帉?Java 并發(fā)代碼的時(shí)候并沒有感覺到這一點(diǎn),這是因?yàn)?Java 語言中有一個(gè)“先行發(fā)生”(Happen-Before) 的原則。這個(gè)原則非常重要,它是判斷數(shù)據(jù)是否存在競(jìng)爭(zhēng),線程是否安全的主要依據(jù)。依靠這個(gè)原則,我們可以通過幾條規(guī)則一次性地解決并發(fā)環(huán)境下兩個(gè)操作之間是否可能存在沖突的所有問題。
先行發(fā)生是 Java 內(nèi)存模型中定義的兩項(xiàng)操作之間的偏序關(guān)系,如果說操作 A 先行發(fā)生于操作 B,其實(shí)就是說在發(fā)生操作 B 之前,操作 A 產(chǎn)生的影響能被操作 B 觀察到,“影響”包括修改了內(nèi)存中共享變量的值、發(fā)送了消息、調(diào)用了方法等。
// 以下操作在線程 A 中執(zhí)行 k = 1; // 以下操作在線程 B 中執(zhí)行 j = k; // 以下操作在線程 C 中執(zhí)行 k = 2;
假設(shè)線程 A 中的操作“k=1”先行發(fā)生于線程 B 的操作“j=k”,那么可以確定在線程 B 的操作執(zhí)行后,變量 j 的值一定等于 1,得出這個(gè)結(jié)論的依據(jù)有兩個(gè):一是根據(jù)先行發(fā)生原則,“k=1”的結(jié)果可以被觀察到;二是線程 C 還沒“登場(chǎng)”,線程 A 操作結(jié)束之后沒有其他線程會(huì)修改變量 k 的值?,F(xiàn)在再來考慮線程 C,我們依然保持線程 A 和線程 B 之間的先行發(fā)生關(guān)系,而線程 C 出現(xiàn)在線程 A 和線程 B 的操作之間,但是線程 C 與線程 B 沒有先行發(fā)生關(guān)系,那 j 的值會(huì)是多少呢?答案是不確定!1 和 2 都有可能,因?yàn)榫€程 C 對(duì)變量 k 的影響可能會(huì)被線程 B 觀察到,也可能不會(huì),這時(shí)候線程 B 就存在讀取到過期數(shù)據(jù)的風(fēng)險(xiǎn),不具備多線程安全性。
下面是 Java 內(nèi)存模型下一些“天然的”先行發(fā)生關(guān)系,這些先行發(fā)生關(guān)系無須任何同步器協(xié)助就已經(jīng)存在,可以在編碼中直接使用。如果兩個(gè)操作之間的關(guān)系不在此列,并且無法從下列規(guī)則推導(dǎo)出來的話,它們就沒有順序性保障,虛擬機(jī)可以對(duì)它們隨意地進(jìn)行重排序。
- 程序次序規(guī)則(Program Order Rule):在一個(gè)線程內(nèi),按照程序代碼順序,書寫在前面的操作先行發(fā)生于書寫在后面的操作。準(zhǔn)確地說,應(yīng)該是控制流順序而不是程序代碼順序,因?yàn)橐紤]分支、循環(huán)等結(jié)構(gòu)。
- 管程鎖定規(guī)則(Monitor Lock Rule):一個(gè) unlock 操作先行發(fā)生于后面對(duì)同一個(gè)鎖的 lock 操作。這里必須強(qiáng)調(diào)的是同一個(gè)鎖,而“后面”是指時(shí)間上的先后順序。
- volatile 變量規(guī)則(Volatile Variable Rule):對(duì)一個(gè) volatile 變量的寫操作先行發(fā)生于后面對(duì)這個(gè)變量的讀操作,這里的“后面”同樣是指時(shí)間上的先后順序。
- 線程啟動(dòng)規(guī)則(Thread Start Rule):Thread 對(duì)象的 start() 方法先行發(fā)生于此線程的每一個(gè)動(dòng)作。
- 線程終止規(guī)則(Thread Termination Rule):線程中的所有操作都先行發(fā)生于對(duì)此線程的終止檢測(cè),我們可以通過 Thread.join() 方法結(jié)束、Thread.isAlive() 的返回值等手段檢測(cè)到線程已經(jīng)終止執(zhí)行。
- 線程中斷規(guī)則(Thread Interruption Rule):對(duì)線程 interrupt() 方法的調(diào)用先行發(fā)生于被中斷線程的代碼檢測(cè)到中斷事件的發(fā)生,可以通過 Thread.interrupted() 方法檢測(cè)到是否有中斷發(fā)生。
- 對(duì)象終結(jié)規(guī)則(Finalizer Rule):一個(gè)對(duì)象的初始化完成(構(gòu)造函數(shù)執(zhí)行結(jié)束)先行發(fā)生于它的 finalize() 方法的開始。
- 傳遞性(Transitivity):如果操作 A 先行發(fā)生于操作 B,操作 B 先行發(fā)生于操作 C,那就可以得出操作 A 先行發(fā)生于操作 C 的結(jié)論。
private int value = 0; pubilc void setValue(int value) { this.value = value; } public int getValue() { return value; }
上述代碼顯示的是一組再普通不過的 getter/setter 方法,假設(shè)存在線程 A 和 B,線程 A 先(時(shí)間上的先后)調(diào)用了“setValue(1)”,然后線程 B 調(diào)用了同一個(gè)對(duì)象的“getValue()”,那么線程 B 收到的返回值是什么?
我們依次分析一下先行發(fā)生原則中的各項(xiàng)規(guī)則,由于兩個(gè)方法分別由線程 A 和線程 B 調(diào)用,不在一個(gè)線程中,所以程序次序規(guī)則在這里不適用;由于沒有同步塊,自然就不會(huì)發(fā)生 lock 和 unlock 操作,所以管程鎖定規(guī)則不適用;由于 value 變量沒有被 volatile 關(guān)鍵字修飾,所以 volatile 變量規(guī)則不適用;后面的線程啟動(dòng)、終止、中斷規(guī)則和對(duì)象終結(jié)規(guī)則也和這里完全沒有關(guān)系。因?yàn)闆]有一個(gè)適用的先行發(fā)生規(guī)則,所以最后一條傳遞性也無從談起,因此我們可以判定盡管線程 A 在操作時(shí)間上先于線程 B,但是無法確定線程 B 中“getValue()”方法的返回結(jié)果,換句話說,這里面的操作不是線程安全的。
那怎么修復(fù)這個(gè)問題呢?我們至少有兩種比較簡(jiǎn)單的方案可以選擇:要么把 getter/setter 方法都定義為 synchronized 方法,這樣就可以套用管程鎖定規(guī)則;要么把 value 定義為 volatile 變量,由于 setter 方法對(duì) value 的修改不依賴 value 的原值,滿足 volatile 關(guān)鍵字使用場(chǎng)景,這樣就可以套用 volatile 變量規(guī)則來實(shí)現(xiàn)先行發(fā)生關(guān)系。
通過上面的例子,我們可以得出結(jié)論:一個(gè)操作“時(shí)間上的先發(fā)生”不代表這個(gè)操作會(huì)是“先行發(fā)生”,那如果一個(gè)操作“先行發(fā)生”是否就能推導(dǎo)出這個(gè)操作必定是“時(shí)間上的先發(fā)生”呢?很遺憾,這個(gè)推論也是不成立的,一個(gè)典型的例子就是多次提到的“指令重排序”。
// 以下操作在同一個(gè)線程中執(zhí)行 int i = 1; int j = 2;
代碼清單的兩條賦值語句在同一個(gè)線程之中,根據(jù)程序次序規(guī)則,“int i=1”的操作先行發(fā)生于“int j=2”,但是“int j=2”的代碼完全可能先被處理器執(zhí)行,這并不影響先行發(fā)生原則的正確性,因?yàn)槲覀冊(cè)谶@條線程之中沒有辦法感知到這點(diǎn)。
上面兩個(gè)例子綜合起來證明了一個(gè)結(jié)論:時(shí)間先后順序與先行發(fā)生原則之間基本沒有太大的關(guān)系,所以我們衡量并發(fā)安全問題的時(shí)候不要受到時(shí)間順序的干擾,一切必須以先行發(fā)生原則為準(zhǔn)。