概述
Java虛擬機(jī)規(guī)范試圖定義一種Java內(nèi)存模型(JMM)來屏蔽掉各種硬件和操作系統(tǒng)的內(nèi)存訪問差異踪栋,以實(shí)現(xiàn)讓Java程序在各種平臺下都能達(dá)到一致的內(nèi)存訪問效果刻伊。
主內(nèi)存和工作內(nèi)存
Java內(nèi)存模型的主要目標(biāo)是定義程序中各個(gè)變量的訪問規(guī)則搬男,即在虛擬機(jī)中將變量存儲到內(nèi)存和從內(nèi)存中取出變量這樣的底層細(xì)節(jié)腺兴。此處的變量與Java編程中所說的變量有所區(qū)別悼尾,它包括了實(shí)例字段傻铣、靜態(tài)字段和構(gòu)成數(shù)組對象的元素章贞,但不包括局部變量和方法參數(shù),因?yàn)楹笳呤蔷€程私有的非洲,不會被共享鸭限,自然就不存在競爭問題。
Java內(nèi)存模型規(guī)定了所有的變量都是存儲在主內(nèi)存中两踏。每條線程還有自己的工作內(nèi)存败京,線程的工作內(nèi)存中保存了該線程使用到的變量的主內(nèi)存副本拷貝,線程對變量的所有操作都必須在工作內(nèi)存中進(jìn)行梦染,而不能直接讀寫主內(nèi)存中的變量赡麦。不同的線程之間也無法直接訪問對方工作內(nèi)存中的變量,線程間變量值的傳遞均需要通過主內(nèi)存來完成帕识。
這里所講的主內(nèi)存泛粹、工作內(nèi)存與Java堆、棧肮疗、方法區(qū)等并不是同一個(gè)層次的內(nèi)存劃分戚扳,這兩者基本上沒有任何關(guān)系。
內(nèi)存間交互操作
Java內(nèi)存模型中定義了以下八種操作族吻,虛擬機(jī)實(shí)現(xiàn)時(shí)必須保證下面提及的每一種操作都是原子的帽借、不可再分的(對于double和long類型的變量來說有例外):
- lock(鎖定)
- unlock(解鎖)
- read(讀取)
- load(載入)
- use(使用)
- assign(復(fù)制)
- store(賦值)
- write(寫入)
Java內(nèi)存模型還規(guī)定了在執(zhí)行上訴八種基本操作時(shí)必須滿足以下規(guī)則:
- 不允許read超歌、load砍艾、store 和 write 操作之一單獨(dú)出現(xiàn),即不允許一個(gè)變量從主內(nèi)存讀取了但工作內(nèi)存不接受巍举,或者從工作內(nèi)存發(fā)起了回寫但主內(nèi)存不接受的情況脆荷。
- 不允許一個(gè)線程丟棄它最近的assign操作,即變量在工作內(nèi)存中改變了之后必須把該變化同步回主內(nèi)存。
- 不允許一個(gè)線程無原因的把數(shù)據(jù)從線程的工作內(nèi)存同步回主內(nèi)存中蜓谋。
- 一個(gè)新的變量只能在主內(nèi)存中誕生梦皮,不允許在工作內(nèi)存中直接使用一個(gè)未被初始化的變量。
- 一個(gè)變量在同一時(shí)刻只允許一條線程對其進(jìn)行l(wèi)ock操作桃焕,但lock操作可以被同一條線程重復(fù)執(zhí)行多次剑肯,多次執(zhí)行l(wèi)ock后,只有執(zhí)行相同次數(shù)的unlock操作观堂,變量才會被解鎖让网。
- 如果對一個(gè)變量執(zhí)行l(wèi)ock操作,那么將會清空工作內(nèi)存中此變量的值师痕,在執(zhí)行引擎使用這個(gè)變量前溃睹,需要重新執(zhí)行l(wèi)oad或assign操作初始化變量的值。
- 如果一個(gè)變量事先沒有被lock操作鎖定胰坟,那就不允許對它執(zhí)行unlock操作因篇,也不允許去unlock一個(gè)被其他線程鎖定住的變量。
- 對一個(gè)變量執(zhí)行unlock操作之前笔横,必須把此變量同步回主內(nèi)存中惜犀。
對于volatile型變量的特殊規(guī)則
當(dāng)一個(gè)變量定義為volatile之后,它將具備兩種特性:第一是保證此變量對所有線程的可見性狠裹,這里的可見性是指當(dāng)一條線程修改了這個(gè)變量的值,新值對于其他線程來說是可以立即得知的汽烦。而普通變量的值在線程間傳遞均需要通過主內(nèi)存來完成涛菠。
Java里面的運(yùn)算并非是原子操作,導(dǎo)致volatile變量的運(yùn)算在并發(fā)下一樣是不安全的撇吞。
由于volatile變量只能保證可見效俗冻,在不符合以下兩條規(guī)則的運(yùn)算場景下,我們?nèi)匀灰ㄟ^加鎖來保證原子性牍颈。
- 運(yùn)算結(jié)果并不依賴變量的當(dāng)前值迄薄,或者能夠確保只有單一的線程修改變量的值
- 變量不需要與其他的狀態(tài)變量共同參與不變約束
第二個(gè)特性是禁止指令重排優(yōu)化。普通的變量僅僅會保證在該方法執(zhí)行過程中所有依賴賦值結(jié)果的地方都能獲取到正確的結(jié)果煮岁,而不能保證變量賦值操作的順序與程序代碼中的執(zhí)行順序一致讥蔽。因?yàn)樵谝粋€(gè)線程的方法執(zhí)行過程中無法感知到這一點(diǎn),這也就是Java內(nèi)存模型中描述的所謂的“線程內(nèi)表現(xiàn)為串行的語義”(As - if - Serial)画机。
volatile 變量讀操作的性能消耗與普通變量幾乎沒有什么差別冶伞,但是寫操作則可能會慢一些,因?yàn)樗枰诒镜卮a中插入很多內(nèi)存屏障指令來保證處理器不發(fā)生亂序執(zhí)行步氏。
對于long和double型變量的特殊規(guī)則
對于64位的數(shù)據(jù)類型响禽,在模型中定義了一種相對寬松的規(guī)定:允許虛擬機(jī)將沒有被volatile修飾的64位數(shù)據(jù)的讀寫操作劃分為兩次32位的操作來進(jìn)行,即可以不保證64位數(shù)據(jù)類型的操作的原子性。
但是目前虛擬機(jī)都幾乎把64位數(shù)據(jù)的讀寫操作作為原子操作來對待芋类,因此我們在編寫代碼中并不需要把long和double變量專門聲明為volatile隆嗅。
原子性、可見性與有序性
-
原子性
我們大致可以認(rèn)為基本數(shù)據(jù)類型的訪問讀寫是具備原子性的侯繁。如果應(yīng)用場景需要一個(gè)更大范圍的原子性保證胖喳,Java內(nèi)存模型還提供了lock和unlock操作來滿足這種需求,盡管虛擬機(jī)未把lock和unlock操作直接開放給用戶使用巫击,但是卻提供了更高層次的字節(jié)碼指令monitorenter和monitorexit來隱式的使用這兩種操作禀晓,這兩個(gè)字節(jié)碼指令就對應(yīng)Java中的synchronized關(guān)鍵字。
-
可見性
可見性是指當(dāng)一個(gè)線程修改了共享變量的值坝锰,其他線程能夠立即得知這個(gè)修改粹懒。Java內(nèi)存模型是通過在變量修改后將新值同步回主內(nèi)存,在變量讀取前從主內(nèi)存刷新變量值這種依賴主內(nèi)存作為傳遞媒介的方式來實(shí)現(xiàn)可見性顷级,無論是普通變量還是volatile變量都是如此凫乖,volatile變量的特殊規(guī)則是保證了新值能立即同步到主內(nèi)存,以及每次使用前立即從主內(nèi)存刷新弓颈。因此帽芽,可以說volatile變量保證了多線程操作時(shí)的可見性,而普通變量則不能保證這一點(diǎn)翔冀。
除了volatile之外导街,Java還有兩個(gè)關(guān)鍵字能實(shí)現(xiàn)可見性,即synchronized和final纤子。同步塊的可見性是由“對變量執(zhí)行unlock操作之前搬瑰,必須先把此變量同步回主內(nèi)存中”這條規(guī)則獲得的,而final關(guān)鍵字的可見性是指:被final修飾的字段在構(gòu)造器中一旦初始化完成控硼,并且構(gòu)造器沒有把this引用傳遞出去泽论,那么其他線程中就能看見final字段的值。
-
有序性
如果在本線程內(nèi)觀察卡乾,所有的操作都是有序的翼悴;如果在一個(gè)線程中觀察另一個(gè)線程,所有的操作都是無序的幔妨。前半句是指“線程內(nèi)表現(xiàn)為串行的語義(As - If - Serial)”鹦赎,后半句是指“指令重排序”現(xiàn)象和“工作內(nèi)存和主內(nèi)存同步延遲”現(xiàn)象。
Java語言提供了volatile和synchronized兩個(gè)關(guān)鍵字來保證線程之前操作的有序性误堡,volatile關(guān)鍵字本身就包含了禁止指令重排序的語義钙姊,而synchronized則是由“一個(gè)變量在同一個(gè)時(shí)刻只允許一條線程對其進(jìn)行l(wèi)ock操作”這條規(guī)則獲得的,這條規(guī)則決定了持有同一個(gè)鎖的兩個(gè)同步塊只能串行的進(jìn)入埂伦。
先行發(fā)生原則
先行發(fā)生是Java內(nèi)存模型中定義的兩項(xiàng)操作之間的偏序關(guān)系煞额。如果說操作A先行發(fā)生與B,那么操作A產(chǎn)生的影響能夠被B觀察到。下面是Java內(nèi)存模型中天然的先行發(fā)生關(guān)系:
-
程序次序規(guī)則
即程序的書寫順序膊毁。
-
管程鎖定規(guī)則
一個(gè)unlock操作先行發(fā)生與后面對同一個(gè)鎖的lock操作胀莹。
-
volatile變量規(guī)則
對一個(gè)volatile變量的寫操作先行發(fā)生與后面對這個(gè)變量的讀操作。
-
線程啟動規(guī)則
Thread.start()方法先行發(fā)生與此線程的每一個(gè)動作婚温。