文章首發(fā)于51CTO技術棧
作者 陳彩華
一付燥、內存模型產生背景
在介紹Java內存模型之前键科,我們先了解一下物理計算機中的并發(fā)問題勋颖,理解這些問題可以搞清楚內存模型產生的背景牙言。物理機遇到的并發(fā)問題與虛擬機中的情況有不少相似之處,物理機的解決方案對虛擬機的實現(xiàn)有相當?shù)膮⒖家饬x卑硫。
物理機的并發(fā)問題
- 硬件的效率問題
計算機處理器處理絕大多數(shù)運行任務都不可能只靠處理器“計算”就能完成欢伏,處理器至少需要與內存交互硝拧,如讀取運算數(shù)據(jù)障陶、存儲運算結果抱究,這個I/O操作很難消除(無法僅靠寄存器完成所有運算任務)鼓寺。
由于計算機的存儲設備與處理器的運算速度有幾個數(shù)量級的差距妈候,為了避免處理器等待緩慢的內存讀寫操作完成苦银,現(xiàn)代計算機系統(tǒng)通過加入一層讀寫速度盡可能接近處理器運算速度的高速緩存墓毒。緩存作為內存和處理器之間的緩沖:將運算需要使用到的數(shù)據(jù)復制到緩存中所计,讓運算能快速運行团秽,當運算結束后再從緩存同步回內存之中。
- 緩存一致性問題
基于高速緩存的存儲系統(tǒng)交互很好地解決了處理器與內存速度的矛盾眷唉,但是也為計算機系統(tǒng)帶來更高的復雜度冬阳,因為引入了一個新問題:緩存一致性肝陪。
在多處理器的系統(tǒng)中(或者單處理器多核的系統(tǒng))氯窍,每個處理器(每個核)都有自己的高速緩存狼讨,而它們有共享同一主內存(Main Memory)熊楼。當多個處理器的運算任務都涉及同一塊主內存區(qū)域時,將可能導致各自的緩存數(shù)據(jù)不一致踩晶。 為此渡蜻,需要各個處理器訪問緩存時都遵循一些協(xié)議茸苇,在讀寫時要根據(jù)協(xié)議進行操作学密,來維護緩存的一致性腻暮。
- 代碼亂序執(zhí)行優(yōu)化問題
為了使得處理器內部的運算單元盡量被充分利用试幽,提高運算效率铺坞,處理器可能會對輸入的代碼進行亂序執(zhí)行康震,處理器會在計算之后將亂序執(zhí)行的結果重組腿短,亂序優(yōu)化可以保證在單線程下該執(zhí)行結果與順序執(zhí)行的結果是一致的橘忱,但不保證程序中各個語句計算的先后順序與輸入代碼中的順序一致钝诚。
亂序執(zhí)行技術是處理器為提高運算速度而做出違背代碼原有順序的優(yōu)化凝颇。在單核時代潘拱,處理器保證做出的優(yōu)化不會導致執(zhí)行結果遠離預期目標,但在多核環(huán)境下卻并非如此拧略。
多核環(huán)境下芦岂, 如果存在一個核的計算任務依賴另一個核 計的算任務的中間結果,而且對相關數(shù)據(jù)讀寫沒做任何防護措施垫蛆,那么其順序性并不能靠代碼的先后順序來保證,處理器最終得出的結果和我們邏輯得到的結果可能會大不相同袱饭。
以上圖為例進行說明:CPU的core2中的邏輯B依賴core1中的邏輯A先執(zhí)行
- 正常情況下,邏輯A執(zhí)行完之后再執(zhí)行邏輯B虑乖。
- 在處理器亂序執(zhí)行優(yōu)化情況下懦趋,有可能導致flag提前被設置為true,導致邏輯B先于邏輯A執(zhí)行决左。
二愕够、Java內存模型的組成分析
內存模型概念
為了更好解決上面提到系列問題走贪,內存模型被總結提出,我們可以把內存模型理解為在特定操作協(xié)議下惑芭,對特定的內存或高速緩存進行讀寫訪問的過程抽象坠狡。
不同架構的物理計算機可以有不一樣的內存模型,Java虛擬機也有自己的內存模型遂跟。Java虛擬機規(guī)范中試圖定義一種Java內存模型(Java Memory Model逃沿,簡稱JMM)來屏蔽掉各種硬件和操作系統(tǒng)的內存訪問差異,以實現(xiàn)讓Java程序在各種平臺下都能達到一致的內存訪問效果幻锁,不必因為不同平臺上的物理機的內存模型的差異凯亮,對各平臺定制化開發(fā)程序。
更具體一點說哄尔,Java內存模型提出目標在于假消,定義程序中各個變量的訪問規(guī)則,即在虛擬機中將變量存儲到內存和從內存中取出變量這樣的底層細節(jié)岭接。此處的變量(Variables)與Java編程中所說的變量有所區(qū)別富拗,它包括了實例字段、靜態(tài)字段和構成數(shù)值對象的元素鸣戴,但不包括局部變量與方法參數(shù)啃沪,因為后者是線程私有的。(如果局部變量是一個reference類型窄锅,它引用的對象在Java堆中可被各個線程共享创千,但是reference本身在Java棧的局部變量表中,它是線程私有的)入偷。
Java內存模型的組成
- 主內存 Java內存模型規(guī)定了所有變量都存儲在主內存(Main Memory)中(此處的主內存與介紹物理硬件的主內存名字一樣追驴,兩者可以互相類比,但此處僅是虛擬機內存的一部分)盯串。
- 工作內存 每條線程都有自己的工作內存(Working Memory氯檐,又稱本地內存,可與前面介紹的處理器高速緩存類比)体捏,線程的工作內存中保存了該線程使用到的變量的主內存中的共享變量的副本拷貝。工作內存是 JMM 的一個抽象概念糯崎,并不真實存在几缭。它涵蓋了緩存,寫緩沖區(qū)沃呢,寄存器以及其他的硬件和編譯器優(yōu)化年栓。
Java內存模型抽象示意圖如下:
JVM內存操作的并發(fā)問題
結合前面介紹的物理機的處理器處理內存的問題薄霜,可以類比總結出JVM內存操作的問題某抓,下面介紹的Java內存模型的執(zhí)行處理將圍繞解決這2個問題展開:
- 1 工作內存數(shù)據(jù)一致性 各個線程操作數(shù)據(jù)時會保存使用到的主內存中的共享變量副本纸兔,當多個線程的運算任務都涉及同一個共享變量時,將導致各自的的共享變量副本不一致否副,如果真的發(fā)生這種情況汉矿,數(shù)據(jù)同步回主內存以誰的副本數(shù)據(jù)為準? Java內存模型主要通過一系列的數(shù)據(jù)同步協(xié)議备禀、規(guī)則來保證數(shù)據(jù)的一致性洲拇,后面再詳細介紹。
- 2 指令重排序優(yōu)化 Java中重排序通常是編譯器或運行時環(huán)境為了優(yōu)化程序性能而采取的對指令進行重新排序執(zhí)行的一種手段曲尸。重排序分為兩類:編譯期重排序和運行期重排序赋续,分別對應編譯時和運行時環(huán)境另患。 同樣的纽乱,指令重排序不是隨意重排序,它需要滿足以下兩個條件: 1 在單線程環(huán)境下不能改變程序運行的結果 即時編譯器(和處理器)需要保證程序能夠遵守 as-if-serial 屬性昆箕。通俗地說鸦列,就是在單線程情況下,要給程序一個順序執(zhí)行的假象为严。即經(jīng)過重排序的執(zhí)行結果要與順序執(zhí)行的結果保持一致敛熬。 2 存在數(shù)據(jù)依賴關系的不允許重排序
多線程環(huán)境下,如果線程處理邏輯之間存在依賴關系第股,有可能因為指令重排序導致運行結果與預期不同应民,后面再展開Java內存模型如何解決這種情況。
三夕吻、 Java內存間的交互操作
在理解Java內存模型的系列協(xié)議诲锹、特殊規(guī)則之前,我們先理解Java中內存間的交互操作涉馅。
交互操作流程
為了更好理解內存的交互操作归园,以線程通信為例,我們看看具體如何進行線程間值的同步:
線程1和線程2都有主內存中共享變量x的副本庸诱,初始時,這3個內存中x的值都為0晤揣。線程1中更新x的值為1之后同步到線程2主要涉及2個步驟:
- 1 線程1把線程工作內存中更新過的x的值刷新到主內存中
- 2 線程2到主內存中讀取線程1之前已更新過的x變量
從整體上看桥爽,這2個步驟是線程1在向線程2發(fā)消息,這個通信過程必須經(jīng)過主內存昧识。線程對變量的所有操作(讀取钠四,賦值)都必須在工作內存中進行。不同線程之間也無法直接訪問對方工作內存中的變量跪楞,線程間變量值的傳遞均需要通過主內存來完成缀去,實現(xiàn)各個線程提供共享變量的可見性侣灶。
內存交互的基本操作
關于主內存與工作內存之間的具體交互協(xié)議,即一個變量如何從主內存拷貝到工作內存缕碎、如何從工作內存同步回主內存之類的實現(xiàn)細節(jié)褥影,Java內存模型中定義了下面介紹8種操作來完成。
虛擬機實現(xiàn)時必須保證下面介紹的每種操作都是原子的阎曹,不可再分的(對于double和long型的變量來說伪阶,load、store处嫌、read栅贴、和write操作在某些平臺上允許有例外,后面會介紹)熏迹。
8種基本操作
- lock (鎖定) 作用于主內存的變量,它把一個變量標識為一條線程獨占的狀態(tài)注暗。
- unlock (解鎖) 作用于主內存的變量坛缕,它把一個處于鎖定狀態(tài)的變量釋放出來,釋放后的變量才可以被其他線程鎖定捆昏。
- read (讀取) 作用于主內存的變量赚楚,它把一個變量的值從主內存傳輸到線程的工作內存中,以便隨后的load動作使用骗卜。
- load (載入) 作用于工作內存的變量宠页,它把read操作從主內存中得到的變量值放入工作內存的變量副本中。
- use (使用) 作用于工作內存的變量寇仓,它把工作內存中一個變量的值傳遞給執(zhí)行引擎举户,每當虛擬機遇到一個需要使用到變量的值得字節(jié)碼指令時就會執(zhí)行這個操作。
- assign (賦值) 作用于工作內存的變量遍烦,它把一個從執(zhí)行引擎接收到的值賦給工作內存的變量俭嘁,每當虛擬機遇到一個給變量賦值的字節(jié)碼指令時執(zhí)行這個操作。
- store (存儲) 作用于工作內存的變量服猪,它把工作內存中一個變量的值傳送到主內存中供填,以便隨后write操作使用。
- write (寫入) 作用于主內存的變量罢猪,它把store操作從工作內存中得到的變量的值放入主內存的變量中捕虽。
四、 Java內存模型運行規(guī)則
4.1 內存交互基本操作的3個特性
在介紹內存的交互的具體的8種基本操作之前坡脐,有必要先介紹一下操作的3個特性,Java內存模型是圍繞著在并發(fā)過程中如何處理這3個特性來建立的房揭,這里先給出定義和基本實現(xiàn)的簡單介紹备闲,后面會逐步展開分析晌端。
- 原子性(Atomicity) 即一個操作或者多個操作 要么全部執(zhí)行并且執(zhí)行的過程不會被任何因素打斷,要么就都不執(zhí)行恬砂。即使在多個線程一起執(zhí)行的時候咧纠,一個操作一旦開始,就不會被其他線程所干擾泻骤。
- 可見性(Visibility) 是指當多個線程訪問同一個變量時漆羔,一個線程修改了這個變量的值,其他線程能夠立即看得到修改的值狱掂。 正如上面“交互操作流程”中所說明的一樣演痒,JMM是通過在線程1變量工作內存修改后將新值同步回主內存,線程2在變量讀取前從主內存刷新變量值趋惨,這種依賴主內存作為傳遞媒介的方式來實現(xiàn)可見性鸟顺。
- 有序性(Ordering) 有序性規(guī)則表現(xiàn)在以下兩種場景: 線程內和線程間 線程內 從某個線程的角度看方法的執(zhí)行,指令會按照一種叫“串行”(as-if-serial)的方式執(zhí)行器虾,此種方式已經(jīng)應用于順序編程語言讯嫂。 線程間 這個線程“觀察”到其他線程并發(fā)地執(zhí)行非同步的代碼時,由于指令重排序優(yōu)化兆沙,任何代碼都有可能交叉執(zhí)行欧芽。唯一起作用的約束是:對于同步方法,同步塊(synchronized關鍵字修飾)以及volatile字段的操作仍維持相對有序葛圃。
Java內存模型的一系列運行規(guī)則看起來有點繁瑣千扔,但總結起來,是圍繞原子性装悲、可見性昏鹃、有序性特征建立。歸根究底诀诊,是為實現(xiàn)共享變量的在多個線程的工作內存的數(shù)據(jù)一致性洞渤,多線程并發(fā),指令重排序優(yōu)化的環(huán)境中程序能如預期運行属瓣。
4.2 happens-before關系
介紹系列規(guī)則之前载迄,首先了解一下happens-before關系:用于描述下2個操作的內存可見性:如果操作A happens-before 操作B,那么A的結果對B可見抡蛙。happens-before關系的分析需要分為單線程和多線程的情況:
- 單線程下的 happens-before 字節(jié)碼的先后順序天然包含happens-before關系:因為單線程內共享一份工作內存护昧,不存在數(shù)據(jù)一致性的問題。 在程序控制流路徑中靠前的字節(jié)碼 happens-before 靠后的字節(jié)碼粗截,即靠前的字節(jié)碼執(zhí)行完之后操作結果對靠后的字節(jié)碼可見惋耙。然而,這并不意味著前者一定在后者之前執(zhí)行。實際上绽榛,如果后者不依賴前者的運行結果湿酸,那么它們可能會被重排序。
- 多線程下的 happens-before 多線程由于每個線程有共享變量的副本灭美,如果沒有對共享變量做同步處理推溃,線程1更新執(zhí)行操作A共享變量的值之后,線程2開始執(zhí)行操作B届腐,此時操作A產生的結果對操作B不一定可見铁坎。
為了方便程序開發(fā),Java內存模型實現(xiàn)了下述支持happens-before關系的操作:
- 程序次序規(guī)則 一個線程內犁苏,按照代碼順序硬萍,書寫在前面的操作 happens-before 書寫在后面的操作。
- 鎖定規(guī)則 一個unLock操作 happens-before 后面對同一個鎖的lock操作傀顾。
- volatile變量規(guī)則 對一個變量的寫操作 happens-before 后面對這個變量的讀操作襟铭。
- 傳遞規(guī)則 如果操作A happens-before 操作B,而操作B又 happens-before 操作C短曾,則可以得出操作A happens-before 操作C寒砖。
- 線程啟動規(guī)則 Thread對象的start()方法 happens-before 此線程的每個一個動作。
- 線程中斷規(guī)則 對線程interrupt()方法的調用 happens-before 被中斷線程的代碼檢測到中斷事件的發(fā)生嫉拐。
- 線程終結規(guī)則 線程中所有的操作都 happens-before 線程的終止檢測哩都,我們可以通過Thread.join()方法結束、Thread.isAlive()的返回值手段檢測到線程已經(jīng)終止執(zhí)行婉徘。
- 對象終結規(guī)則 一個對象的初始化完成 happens-before 他的finalize()方法的開始
4.3 內存屏障
Java中如何保證底層操作的有序性和可見性漠嵌?可以通過內存屏障。
內存屏障是被插入兩個CPU指令之間的一種指令盖呼,用來禁止處理器指令發(fā)生重排序(像屏障一樣)儒鹿,從而保障有序性的。另外几晤,為了達到屏障的效果约炎,它也會使處理器寫入、讀取值之前蟹瘾,將主內存的值寫入高速緩存圾浅,清空無效隊列,從而保障可見性憾朴。
舉個例子:
Store1;
Store2;
Load1;
StoreLoad; //內存屏障
Store3;
Load2;
Load3;
對于上面的一組CPU指令(Store表示寫入指令狸捕,Load表示讀取指令),StoreLoad屏障之前的Store指令無法與StoreLoad屏障之后的Load指令進行交換位置众雷,即重排序灸拍。但是StoreLoad屏障之前和之后的指令是可以互換位置的做祝,即Store1可以和Store2互換,Load2可以和Load3互換株搔。
常見有4種屏障
- LoadLoad屏障: 對于這樣的語句 Load1; LoadLoad; Load2剖淀,在Load2及后續(xù)讀取操作要讀取的數(shù)據(jù)被訪問前,保證Load1要讀取的數(shù)據(jù)被讀取完畢纤房。
- StoreStore屏障: 對于這樣的語句 Store1; StoreStore; Store2,在Store2及后續(xù)寫入操作執(zhí)行前翻诉,保證Store1的寫入操作對其它處理器可見炮姨。
- LoadStore屏障: 對于這樣的語句Load1; LoadStore; Store2,在Store2及后續(xù)寫入操作被執(zhí)行前碰煌,保證Load1要讀取的數(shù)據(jù)被讀取完畢舒岸。
- StoreLoad屏障: 對于這樣的語句Store1; StoreLoad; Load2,在Load2及后續(xù)所有讀取操作執(zhí)行前芦圾,保證Store1的寫入對所有處理器可見蛾派。它的開銷是四種屏障中最大的(沖刷寫緩沖器,清空無效化隊列)个少。在大多數(shù)處理器的實現(xiàn)中洪乍,這個屏障是個萬能屏障,兼具其它三種內存屏障的功能夜焦。
Java中對內存屏障的使用在一般的代碼中不太容易見到壳澳,常見的有volatile和synchronized關鍵字修飾的代碼塊(后面再展開介紹),還可以通過Unsafe這個類來使用內存屏障茫经。
4.4 8種操作同步的規(guī)則
JMM在執(zhí)行前面介紹8種基本操作時巷波,為了保證內存間數(shù)據(jù)一致性,JMM中規(guī)定需要滿足以下規(guī)則:
- 規(guī)則1:如果要把一個變量從主內存中復制到工作內存卸伞,就需要按順序的執(zhí)行 read 和 load 操作抹镊,如果把變量從工作內存中同步回主內存中,就要按順序的執(zhí)行 store 和 write 操作荤傲。但 Java 內存模型只要求上述操作必須按順序執(zhí)行垮耳,而沒有保證必須是連續(xù)執(zhí)行。
- 規(guī)則2:不允許 read 和 load弃酌、store 和 write 操作之一單獨出現(xiàn)氨菇。
- 規(guī)則3:不允許一個線程丟棄它的最近 assign 的操作,即變量在工作內存中改變了之后必須同步到主內存中妓湘。
- 規(guī)則4:不允許一個線程無原因的(沒有發(fā)生過任何 assign 操作)把數(shù)據(jù)從工作內存同步回主內存中查蓉。
- 規(guī)則5:一個新的變量只能在主內存中誕生,不允許在工作內存中直接使用一個未被初始化(load 或 assign )的變量榜贴。即就是對一個變量實施 use 和 store 操作之前豌研,必須先執(zhí)行過了 load 或 assign 操作妹田。
- 規(guī)則6:一個變量在同一個時刻只允許一條線程對其進行 lock 操作,但 lock 操作可以被同一條線程重復執(zhí)行多次鹃共,多次執(zhí)行 lock 后鬼佣,只有執(zhí)行相同次數(shù)的 unlock 操作,變量才會被解鎖霜浴。所以 lock 和 unlock 必須成對出現(xiàn)晶衷。
- 規(guī)則7:如果對一個變量執(zhí)行 lock 操作,將會清空工作內存中此變量的值阴孟,在執(zhí)行引擎使用這個變量前需要重新執(zhí)行 load 或 assign 操作初始化變量的值晌纫。
- 規(guī)則8:如果一個變量事先沒有被 lock 操作鎖定,則不允許對它執(zhí)行 unlock 操作永丝;也不允許去 unlock 一個被其他線程鎖定的變量锹漱。
- 規(guī)則9:對一個變量執(zhí)行 unlock 操作之前,必須先把此變量同步到主內存中(執(zhí)行 store 和 write 操作)
看起來這些規(guī)則有些繁瑣慕嚷,其實也不難理解:
- 規(guī)則1哥牍、規(guī)則2 工作內存中的共享變量作為主內存的副本,主內存變量的值同步到工作內存需要read和load一起使用喝检,工作內存中的變量的值同步回主內存需要store和write一起使用嗅辣,這2組操作各自都是是一個固定的有序搭配,不允許單獨出現(xiàn)蛇耀。
- 規(guī)則3辩诞、規(guī)則4 由于工作內存中的共享變量是主內存的副本,為保證數(shù)據(jù)一致性纺涤,當工作內存中的變量被字節(jié)碼引擎重新賦值译暂,必須同步回主內存。如果工作內存的變量沒有被更新撩炊,不允許無原因同步回主內存外永。
- 規(guī)則5 由于工作內存中的共享變量是主內存的副本,必須從主內存誕生拧咳。
- 規(guī)則6伯顶、7、8骆膝、9 為了并發(fā)情況下安全使用變量祭衩,線程可以基于lock操作獨占主內存中的變量,其他線程不允許使用或unlock該變量阅签,直到變量被線程unlock掐暮。
4.5 volatile型變量的特殊規(guī)則
volatile的中文意思是不穩(wěn)定的,易變的政钟,用volatile修飾變量是為了保證變量的可見性路克。
volatile的語義
volatile主要有下面2種語義
語義1 保證可見性
保證了不同線程對該變量操作的內存可見性樟结。
這里保證可見性是不等同于volatile變量并發(fā)操作的安全性,保證可見性具體一點解釋:
線程寫volatile變量的過程:
- 1 改變線程工作內存中volatile變量副本的值
- 2 將改變后的副本的值從工作內存刷新到主內存
線程讀volatile變量的過程:
- 1 從主內存中讀取volatile變量的最新值到線程的工作內存中
- 2 從工作內存中讀取volatile變量的副本
但是如果多個線程同時把更新后的變量值同時刷新回主內存精算,可能導致得到的值不是預期結果:
舉個例子: 定義volatile int count = 0瓢宦,2個線程同時執(zhí)行count++操作,每個線程都執(zhí)行500次灰羽,最終結果小于1000驮履,原因是每個線程執(zhí)行count++需要以下3個步驟:
- 步驟1 線程從主內存讀取最新的count的值
- 步驟2 執(zhí)行引擎把count值加1,并賦值給線程工作內存
- 步驟3 線程工作內存把count值保存到主內存 有可能某一時刻2個線程在步驟1讀取到的值都是100谦趣,執(zhí)行完步驟2得到的值都是101疲吸,最后刷新了2次101保存到主內存。
語義2 禁止進行指令重排序
具體一點解釋前鹅,禁止重排序的規(guī)則如下:
- 當程序執(zhí)行到 volatile變量的讀操作或者寫操作時,在其前面的操作的更改肯定全部已經(jīng)進行峭梳,且結果已經(jīng)對后面的操作可見舰绘;在其后面的操作肯定還沒有進行;
- 在進行指令優(yōu)化時葱椭,不能將在對 volatile 變量訪問的語句放在其后面執(zhí)行捂寿,也不能把 volatile 變量后面的語句放到其前面執(zhí)行。
普通的變量僅僅會保證該方法的執(zhí)行過程中所有依賴賦值結果的地方都能獲取到正確的結果孵运,而不能保證賦值操作的順序與程序代碼中的執(zhí)行順序一致秦陋。
舉個例子:
volatile boolean initialized = false;
// 下面代碼線程A中執(zhí)行
// 讀取配置信息,當讀取完成后將initialized設置為true以通知其他線程配置可用
doSomethingReadConfg();
initialized = true;
// 下面代碼線程B中執(zhí)行
// 等待initialized 為true治笨,代表線程A已經(jīng)把配置信息初始化完成
while (!initialized) {
sleep();
}
// 使用線程A初始化好的配置信息
doSomethingWithConfig();
上面代碼中如果定義initialized變量時沒有使用volatile修飾驳概,就有可能會由于指令重排序的優(yōu)化,導致線程A中最后一句代碼 "initialized = true" 在 “doSomethingReadConfg()” 之前被執(zhí)行旷赖,這樣會導致線程B中使用配置信息的代碼就可能出現(xiàn)錯誤顺又,而volatile關鍵字就禁止重排序的語義可以避免此類情況發(fā)生。
volatile型變量實現(xiàn)原理
具體實現(xiàn)方式是在編譯期生成字節(jié)碼時等孵,會在指令序列中增加內存屏障來保證稚照,下面是基于保守策略的JMM內存屏障插入策略:
- 在每個volatile寫操作的前面插入一個StoreStore屏障俯萌。 該屏障除了保證了屏障之前的寫操作和該屏障之后的寫操作不能重排序果录,還會保證了volatile寫操作之前,任何的讀寫操作都會先于volatile被提交咐熙。
- 在每個volatile寫操作的后面插入一個StoreLoad屏障弱恒。 該屏障除了使volatile寫操作不會與之后的讀操作重排序外,還會刷新處理器緩存糖声,使volatile變量的寫更新對其他線程可見斤彼。
- 在每個volatile讀操作的后面插入一個LoadLoad屏障分瘦。 該屏障除了使volatile讀操作不會與之前的寫操作發(fā)生重排序外,還會刷新處理器緩存琉苇,使volatile變量讀取的為最新值嘲玫。
- 在每個volatile讀操作的后面插入一個LoadStore屏障。 該屏障除了禁止了volatile讀操作與其之后的任何寫操作進行重排序并扇,還會刷新處理器緩存去团,使其他線程volatile變量的寫更新對volatile讀操作的線程可見。
volatile型變量使用場景
總結起來穷蛹,就是“一次寫入土陪,到處讀取”,某一線程負責更新變量肴熏,其他線程只讀取變量(不更新變量)鬼雀,并根據(jù)變量的新值執(zhí)行相應邏輯硅则。例如狀態(tài)標志位更新迹卢,觀察者模型變量值發(fā)布位他。
4.6 final型變量的特殊規(guī)則
我們知道筷屡,final成員變量必須在聲明的時候初始化或者在構造器中初始化粮彤,否則就會報編譯錯誤立叛。 final關鍵字的可見性是指:被final修飾的字段在聲明時或者構造器中造壮,一旦初始化完成吼过,那么在其他線程無須同步就能正確看見final字段的值泼诱。這是因為一旦初始化完成坛掠,final變量的值立刻回寫到主內存。
4.7 synchronized的特殊規(guī)則
通過 synchronized關鍵字包住的代碼區(qū)域治筒,對數(shù)據(jù)的讀寫進行控制:
- 讀數(shù)據(jù) 當線程進入到該區(qū)域讀取變量信息時屉栓,對數(shù)據(jù)的讀取也不能從工作內存讀取,只能從內存中讀取矢炼,保證讀到的是最新的值系瓢。
- 寫數(shù)據(jù) 在同步區(qū)內對變量的寫入操作,在離開同步區(qū)時就將當前線程內的數(shù)據(jù)刷新到內存中句灌,保證更新的數(shù)據(jù)對其他線程的可見性夷陋。
4.8 long和double型變量的特殊規(guī)則
Java內存模型要求lock、unlock胰锌、read骗绕、load、assign资昧、use酬土、store、write這8種操作都具有原子性格带,但是對于64位的數(shù)據(jù)類型(long和double)撤缴,在模型中特別定義相對寬松的規(guī)定:允許虛擬機將沒有被volatile修飾的64位數(shù)據(jù)的讀寫操作分為2次32位的操作來進行刹枉。也就是說虛擬機可選擇不保證64位數(shù)據(jù)類型的load、store屈呕、read和write這4個操作的原子性微宝。由于這種非原子性,有可能導致其他線程讀到同步未完成的“32位的半個變量”的值虎眨。
不過實際開發(fā)中蟋软,Java內存模型強烈建議虛擬機把64位數(shù)據(jù)的讀寫實現(xiàn)為具有原子性,目前各種平臺下的商用虛擬機都選擇把64位數(shù)據(jù)的讀寫操作作為原子操作來對待嗽桩,因此我們在編寫代碼時一般不需要把用到的long和double變量專門聲明為volatile岳守。
五、總結
由于Java內存模型涉及系列規(guī)則碌冶,網(wǎng)上的文章大部分就是對這些規(guī)則進行解析湿痢,但是很多沒有解釋為什么需要這些規(guī)則,這些規(guī)則的作用扑庞,其實這是不利于初學者學習的蒙袍,容易繞進去這些繁瑣規(guī)則不知所以然,下面談談我的一點學習知識的個人體會:
學習知識的過程不是等同于只是理解知識和記憶知識嫩挤,而是要對知識解決的問題的輸入和輸出建立連接,知識的本質是解決問題消恍,所以在學習之前要理解問題岂昭,理解這個問題要的輸出和輸出,而知識就是輸入到輸出的一個關系映射狠怨。知識的學習要結合大量的例子來理解這個映射關系约啊,然后壓縮知識,華羅庚說過:“把一本書讀厚佣赖,然后再讀薄”恰矩,解釋的就是這個道理,先結合大量的例子理解知識憎蛤,然后再壓縮知識外傅。
以學習Java內存模型為例:
- 理解問題,明確輸入輸出 首先理解Java內存模型是什么俩檬,有什么用萎胰,解決什么問題
- 理解內存模型系列協(xié)議 結合大量例子理解這些協(xié)議規(guī)則
- 壓縮知識 大量規(guī)則其實就是通過數(shù)據(jù)同步協(xié)議,保證內存副本之間的數(shù)據(jù)一致性棚辽,同時防止重排序對程序的影響技竟。