Java內(nèi)存模型(Java Memory Model ,JMM)就是一種符合內(nèi)存模型規(guī)范的,屏蔽了各種硬件和操作系統(tǒng)的訪問差異的,保證了Java程序在各種平臺下對內(nèi)存的訪問都能保證效果一致的機制及規(guī)范。
Java 內(nèi)存模型的主要目標是定義程序中各個變量的訪問規(guī)則,也就是在虛擬機中將變量存儲到內(nèi)存以及從內(nèi)存中取出變量(這里的變量,指的是共享變量,也就是實例對象、靜態(tài)字段、數(shù)組對象等存儲在堆內(nèi)存中的變量。而對于局部變量這類的,屬于線程私有,不會被共享)這類的底層細節(jié)。通過這些規(guī)則來規(guī)范對內(nèi)存的讀寫操作,從而保證指令執(zhí)行的正確性。
它與處理器有關(guān)、與緩存有關(guān)、與并發(fā)有關(guān)、與編譯器也有關(guān)。他解決了 CPU多級緩存、處理器優(yōu)化、指令重排等導(dǎo)致的內(nèi)存訪問問題,保證了并發(fā)場景下的可見性、原子性和有序性。內(nèi)存模型解決并發(fā)問題主要采用兩種方式:限制處理器優(yōu)化和使用內(nèi)存屏障 。
Java內(nèi)存模型規(guī)定了所有的變量都存儲在主內(nèi)存中,每條線程還有自己的工作內(nèi)存,線程的工作內(nèi)存中保存了該線程中是用到的變量的主內(nèi)存副本拷貝,線程對變量的所有操作都必須在工作內(nèi)存中進行,而不能直接讀寫主內(nèi)存。不同的線程之間也無法直接訪問對方工作內(nèi)存中的變量,線程間變量的傳遞均需要自己的工作內(nèi)存和主存之間進行數(shù)據(jù)同步進行。而JMM就作用于工作內(nèi)存和主存之間數(shù)據(jù)同步過程。他規(guī)定了如何做數(shù)據(jù)同步以及什么時候做數(shù)據(jù)同步。
我們知道CPU的處理速度和主存的讀寫速度不是一個量級的,為了平衡這種巨大的差距,每個CPU都會有緩存。因此,共享變量會先放在主存中,每個線程都有屬于自己的工作內(nèi)存,并且會把位于主存中的共享變量拷貝到自己的工作內(nèi)存,之后的讀寫操作均使用位于工作內(nèi)存的變量副本,并在某個時刻將工作內(nèi)存的變量副本寫回到主存中去。JMM就從抽象層次定義了這種方式,并且JMM決定了一個線程對共享變量的寫入何時對其他線程是可見的。

如圖為JMM抽象示意圖,線程A和線程B之間要完成通信的話,要經(jīng)歷如下兩步:
線程A從主內(nèi)存中將共享變量讀入線程A的工作內(nèi)存后并進行操作,之后將數(shù)據(jù)重新寫回到主內(nèi)存中
線程B從主存中讀取最新的共享變量
從橫向去看看,線程A和線程B就好像通過共享變量在進行隱式通信。這其中有很有意思的問題,如果線程A更新后數(shù)據(jù)并沒有及時寫回到主存,而此時線程B讀到的是過期的數(shù)據(jù),這就出現(xiàn)了“臟讀”現(xiàn)象??梢酝ㄟ^同步機制(控制不同線程間操作發(fā)生的相對順序)來解決或者通過volatile關(guān)鍵字使得每次volatile變量都能夠強制刷新到主存,從而對每個線程都是可見的。
Java 內(nèi)存模型定義了線程和內(nèi)存的交互方式,在 JMM 抽象模型中,分為主內(nèi)存、工作內(nèi)存。主內(nèi)存是所有線程共享的,工作內(nèi)存是每個線程獨有的。線程對變量的所有操作(讀取、賦值)都必須在工作內(nèi)存中進行,不能直接讀寫主內(nèi)存中的變量。并且不同的線程之間無法訪問對方工作內(nèi)存中的變量,線程間的變量值的傳遞都需要通過主內(nèi)存來完成,他們?nèi)叩慕换リP(guān)系如下 :

在進行工作內(nèi)存間數(shù)據(jù)的同步時,會存在以上八種原子操作指令來保證。
總結(jié)下,JMM是一種規(guī)范,目的是解決由于多線程通過共享內(nèi)存進行通信時,存在的本地內(nèi)存數(shù)據(jù)不一致、編譯器會對代碼指令重排序、處理器會對代碼亂序執(zhí)行等帶來的問題。目的是保證并發(fā)編程場景中的原子性、可見性和有序性。
在Java中提供了一系列和并發(fā)處理相關(guān)的關(guān)鍵字,比如volatile、Synchronized、final、juc等,這些就是Java內(nèi)存模型封裝了底層的實現(xiàn)后提供給開發(fā)人員使用的關(guān)鍵字,在開發(fā)多線程代碼的時候,我們可以直接使用synchronized等關(guān)鍵詞來控制并發(fā),使得我們不需要關(guān)心底層的編譯器優(yōu)化、緩存一致性的問題了,所以在Java內(nèi)存模型中,除了定義了一套規(guī)范,還提供了開放的指令在底層進行封裝后,提供給開發(fā)人員使用。
如何保證原子性
在Java中,為了保證原子性,提供了兩個高級的字節(jié)碼指令monitorenter和monitorexit。這兩個字節(jié)碼,在Java中對應(yīng)的關(guān)鍵字就是synchronized。因此,在Java中可以使用synchronized來保證方法和代碼塊內(nèi)的操作是原子性的。
如何保證可見性
Java中的volatile關(guān)鍵字提供了一個功能,那就是被其修飾的變量在被修改后可以立即同步到主內(nèi)存,被其修飾的變量在每次是用之前都從主內(nèi)存刷新。因此,可以使用volatile來保證多線程操作時變量的可見性。除了volatile,Java中的synchronized和final兩個關(guān)鍵字也可以實現(xiàn)可見性。
有序性
可以使用synchronized和volatile來保證多線程之間操作的有序性。實現(xiàn)方式有所區(qū)別:volatile關(guān)鍵字會禁止指令重排。synchronized關(guān)鍵字保證同一時刻只允許一條線程操作。
as-if-serial語義
as-if-serial語義的意思是:不管怎么重排序(編譯器和處理器為了提供并行度),(單線程)程序的執(zhí)行結(jié)果不能被改變。編譯器,runtime和處理器都必須遵守as-if-serial語義。as-if-serial語義把單線程程序保護了起來,遵守as-if-serial語義的編譯器,runtime和處理器共同為編寫單線程程序的程序員創(chuàng)建了一個幻覺:單線程程序是按程序的順序來執(zhí)行的。比如上面計算圓面積的代碼,在單線程中,會讓人感覺代碼是一行一行順序執(zhí)行上,實際上A,B兩行不存在數(shù)據(jù)依賴性可能會進行重排序,即A,B不是順序執(zhí)行的。as-if-serial語義使程序員不必擔心單線程中重排序的問題干擾他們,也無需擔心內(nèi)存可見性問題。
happens-before定義
happens-before的概念最初由Leslie Lamport在其一篇影響深遠的論文(《Time,Clocks and the Ordering of Events in a Distributed System》)中提出,有興趣的可以google一下。JSR-133使用happens-before的概念來指定兩個操作之間的執(zhí)行順序。由于這兩個操作可以在一個線程之內(nèi),也可以是在不同線程之間。因此,JMM可以通過happens-before關(guān)系向程序員提供跨線程的內(nèi)存可見性保證(如果A線程的寫操作a與B線程的讀操作b之間存在happens-before關(guān)系,盡管a操作和b操作在不同的線程中執(zhí)行,但JMM向程序員保證a操作將對b操作可見)。具體的定義為:
1)如果一個操作happens-before另一個操作,那么第一個操作的執(zhí)行結(jié)果將對第二個操作可見,而且第一個操作的執(zhí)行順序排在第二個操作之前。
2)兩個操作之間存在happens-before關(guān)系,并不意味著Java平臺的具體實現(xiàn)必須要按照happens-before關(guān)系指定的順序來執(zhí)行。如果重排序之后的執(zhí)行結(jié)果,與按happens-before關(guān)系來執(zhí)行的結(jié)果一致,那么這種重排序并不非法(也就是說,JMM允許這種重排序)。
上面的1)是JMM對程序員的承諾。從程序員的角度來說,可以這樣理解happens-before關(guān)系:如果A happens-before B,那么Java內(nèi)存模型將向程序員保證——A操作的結(jié)果將對B可見,且A的執(zhí)行順序排在B之前。注意,這只是Java內(nèi)存模型向程序員做出的保證!
上面的2)是JMM對編譯器和處理器重排序的約束原則。正如前面所言,JMM其實是在遵循一個基本原則:只要不改變程序的執(zhí)行結(jié)果(指的是單線程程序和正確同步的多線程程序),編譯器和處理器怎么優(yōu)化都行。JMM這么做的原因是:程序員對于這兩個操作是否真的被重排序并不關(guān)心,程序員關(guān)心的是程序執(zhí)行時的語義不能被改變(即執(zhí)行結(jié)果不能被改變)。因此,happens-before關(guān)系本質(zhì)上和as-if-serial語義是一回事。
比較:
as-if-serial VS happens-before
as-if-serial語義保證單線程內(nèi)程序的執(zhí)行結(jié)果不被改變,happens-before關(guān)系保證正確同步的多線程程序的執(zhí)行結(jié)果不被改變。
as-if-serial語義給編寫單線程程序的程序員創(chuàng)造了一個幻境:單線程程序是按程序的順序來執(zhí)行的。happens-before關(guān)系給編寫正確同步的多線程程序的程序員創(chuàng)造了一個幻境:正確同步的多線程程序是按happens-before指定的順序來執(zhí)行的。
as-if-serial語義和happens-before這么做的目的,都是為了在不改變程序執(zhí)行結(jié)果的前提下,盡可能地提高程序執(zhí)行的并行度。
具體規(guī)則
具體的一共有六項規(guī)則:
程序順序規(guī)則:一個線程中的每個操作,happens-before于該線程中的任意后續(xù)操作。
監(jiān)視器鎖規(guī)則:對一個鎖的解鎖,happens-before于隨后對這個鎖的加鎖。
volatile變量規(guī)則:對一個volatile域的寫,happens-before于任意后續(xù)對這個volatile域的讀。
傳遞性:如果A happens-before B,且B happens-before C,那么A happens-before C。
start()規(guī)則:如果線程A執(zhí)行操作ThreadB.start()(啟動線程B),那么A線程的ThreadB.start()操作happens-before于線程B中的任意操作。
join()規(guī)則:如果線程A執(zhí)行操作ThreadB.join()并成功返回,那么線程B中的任意操作happens-before于線程A從ThreadB.join()操作成功返回。
程序中斷規(guī)則:對線程interrupted()方法的調(diào)用先行于被中斷線程的代碼檢測到中斷時間的發(fā)生。
對象finalize規(guī)則:一個對象的初始化完成(構(gòu)造函數(shù)執(zhí)行結(jié)束)先行于發(fā)生它的finalize()方法的開始。
下面以一個具體的例子來講下如何使用這些規(guī)則進行推論:
double pi = 3.14 //A
double r = 1.0 //B
double area = pi * r * r //C
依舊以上面計算圓面積的進行描述。利用程序順序規(guī)則(規(guī)則1)存在三個happens-before關(guān)系:
- A happens-before B;2. B happens-before C;3. A happens-before C。
這里的第三個關(guān)系是利用傳遞性進行推論的。A happens-before B,定義1要求A執(zhí)行結(jié)果對B可見,并且A操作的執(zhí)行順序在B操作之前,但與此同時利用定義中的第二條,A,B操作彼此不存在數(shù)據(jù)依賴性,兩個操作的執(zhí)行順序?qū)ψ罱K結(jié)果都不會產(chǎn)生影響,在不改變最終結(jié)果的前提下,允許A,B兩個操作重排序,即happens-before關(guān)系并不代表了最終的執(zhí)行順序。
總結(jié)
上面已經(jīng)聊了關(guān)于JMM的兩個方面:1. JMM的抽象結(jié)構(gòu)(主內(nèi)存和線程工作內(nèi)存);2. 重排序以及happens-before規(guī)則。接下來,我們來做一個總結(jié)。從兩個方面進行考慮。1. 如果讓我們設(shè)計JMM應(yīng)該從哪些方面考慮,也就是說JMM承擔哪些功能;2. happens-before與JMM的關(guān)系;3. 由于JMM,多線程情況下可能會出現(xiàn)哪些問題?
JMM的設(shè)計

JMM是語言級的內(nèi)存模型,在我的理解中JMM處于中間層,包含了兩個方面:
(1)內(nèi)存模型;
(2)重排序以及happens-before規(guī)則。同時,為了禁止特定類型的重排序會對編譯器和處理器指令序列加以控制。而上層會有基于JMM的關(guān)鍵字和J.U.C包下的一些具體類用來方便程序員能夠迅速高效率的進行并發(fā)編程。站在JMM設(shè)計者的角度,在設(shè)計JMM時需要考慮兩個關(guān)鍵因素:
程序員對內(nèi)存模型的使用,程序員希望內(nèi)存模型易于理解、易于編程。程序員希望基于一個強內(nèi)存模型來編寫代碼。
編譯器和處理器對內(nèi)存模型的實現(xiàn),編譯器和處理器希望內(nèi)存模型對它們的束縛越少越好,這樣它們就可以做盡可能多的優(yōu)化來提高性能。編譯器和處理器希望實現(xiàn)一個弱內(nèi)存模型。
另外還要一個特別有意思的事情就是關(guān)于重排序問題,更簡單的說,重排序可以分為兩類:
會改變程序執(zhí)行結(jié)果的重排序。
不會改變程序執(zhí)行結(jié)果的重排序。
JMM對這兩種不同性質(zhì)的重排序,采取了不同的策略,如下。
對于會改變程序執(zhí)行結(jié)果的重排序,JMM要求編譯器和處理器必須禁止這種重排序。
對于不會改變程序執(zhí)行結(jié)果的重排序,JMM對編譯器和處理器不做要求(JMM允許這種
重排序)

從圖可以看出:
JMM向程序員提供的happens-before規(guī)則能滿足程序員的需求。JMM的happens-before規(guī)則不但簡單易懂,而且也向程序員提供了足夠強的內(nèi)存可見性保證(有些內(nèi)存可見性保證其實并不一定真實存在,比如上面的A happens-before B)。
JMM對編譯器和處理器的束縛已經(jīng)盡可能少。從上面的分析可以看出,JMM其實是在遵循一個基本原則:只要不改變程序的執(zhí)行結(jié)果(指的是單線程程序和正確同步的多線程程序),編譯器和處理器怎么優(yōu)化都行。例如,如果編譯器經(jīng)過細致的分析后,認定一個鎖只會被單個線程訪問,那么這個鎖可以被消除。再如,如果編譯器經(jīng)過細致的分析后,認定一個volatile變量只會被單個線程訪問,那么編譯器可以把這個volatile變量當作一個普通變量來對待。這些優(yōu)化既不會改變程序的執(zhí)行結(jié)果,又能提高程序的執(zhí)行效率。
happens-before與JMM的關(guān)系

一個happens-before規(guī)則對應(yīng)于一個或多個編譯器和處理器重排序規(guī)則。對于Java程序員來說,happens-before規(guī)則簡單易懂,它避免Java程序員為了理解JMM提供的內(nèi)存可見性保證而去學習復(fù)雜的重排序規(guī)則以及這些規(guī)則的具體實現(xiàn)方法
大部分取材自以下:
鏈接:http://www.itdecent.cn/p/797129612bed
總結(jié)得很好,受益良多。