前言:JVM內存模型、Java內存區(qū)域、GC分代回收容易搞混。前面講解了JVM內存區(qū)域,它是Java代碼編譯成.class字節(jié)碼之后JVM運行時的一些實現。JVM內存區(qū)域實際上可以理解成JVM運行時數據區(qū),即方法區(qū)、堆內存、虛擬機棧、本地方法棧、程序計數器等內容。而GC分代回收所描述的更多是垃圾回收器管理堆內存的方式,如堆內存被分為老年代、新生代(又分為Eden、From Survivor、To Survivor區(qū)),下面的部分將說明JVM內存模型(JMM)是什么。
8.1 Java內存模型引入
8.1.1 多線程引起的問題
public class VisibilityDemo {
private boolean flag = true;
public static void main(String args[]) throws InterruptedException {
VisibilityDemo demo = new VisibilityDemo();
Thread thread = new Thread(() -> {
int i = 0;
while(demo.flag){
i++;
}
System.out.println(i);
}
);
thread.start();
TimeUnit.SECONDS.sleep(2);
demo.flag = false;
System.out.println("flag被設置為" + demo.flag);
}
}
執(zhí)行結果:

從結果可以看到,程序一直沒有執(zhí)行完,實際上flag已經被修改為false了,但是修改沒有生效,心中無數只草泥馬走過……
可能的原因:
首先前面的章節(jié)已經介紹過了,每個CPU核心有一級緩存、二級緩存、三級緩存(共享),還有主內存,這里有可能的是Main函數被核心1調用,而thread被核心2調用,程序剛開始加載的時候,會將flag的值加載到主內存中,核心1獲取到了flag的值是true,核心1將flag的值保存在了它自己的緩存中,然后核心2將數據修改了并且刷新到了主內存,然后flag又被核心2放到它自己的緩存中并打印出來。
但是這種情況只會導致thread工作內存中的flag與主內存中的flag短暫的不一致,而不會導致while循環(huán)一直沒法結束。
注意:CPU緩存間有最終一致的保證,并不會導致一直不一致。
我們執(zhí)行程序的時候總會有一種假設:想象在程序中只存在唯一的操作執(zhí)行順序,而不考慮這些操作在何種處理器上執(zhí)行,并且在每次讀取變量時,都能獲得在執(zhí)行序列中最近一次寫入該變量的值,這種樂觀的模型就是“串行一致性”。但是事實上,沒有任何一款現代多核處理器架構中會提供這種“串行一致性”。
Java內存模型(Java Memory Model,JMM)規(guī)定了JVM必須遵循一組最小保證,這組保證規(guī)定了對變量的寫入操作在何時將對于其他線程可見。JMM在設計時就在可預測性和程序的易于開發(fā)性之間進行了權衡,從而在各種主流的處理器體系架構上能實現高性能的JVM。
JMM內存模型是通過各種操作來定義的,包括對變量的讀/寫操作,監(jiān)視器的加鎖和釋放操作,以及線程的啟動和合并操作。JMM為程序中所有的操作定義了一個“Happen Before”規(guī)則。要想保證執(zhí)行操作B的線程看到操作A的結果(無論A和B是否在同一個線程中執(zhí)行),那么在A和B之間必須滿足Happens-Before關系。如果兩個操作之間缺乏Happens-Before關系,那么JVM可以對它們任意地重排(指令重排序,后面說明,請耐心往下閱讀)。
8.1.2 Happens-Before先行發(fā)生原則
既然先提到了Happens-Before,先介紹一下有哪些原則:
程序順序規(guī)則:如果程序中操作A在操作B之前,那么線程中A操作將在B操作之前執(zhí)行
監(jiān)視器鎖規(guī)則:在監(jiān)視器鎖上的解鎖操作必須在同一個監(jiān)視器鎖上的加鎖操作之前執(zhí)行
volatile變量規(guī)則:對volatile變量的吸入操作必須對該變量的讀操作之前執(zhí)行,
線程啟動規(guī)則:在線程上對Thread.Start的調用必須在該線程中執(zhí)行任何操作之前執(zhí)行。
線程結束規(guī)則:線程中的任何操作都必須在其他線程檢測到該線程已經結束之前執(zhí)行,或者從Thread.join中成功返回,或者在調用Thread.isAlive時返回false
中斷規(guī)則:當一個線程在另一個線程上調用interrupt時,必須在被中斷線程檢測到interrupt調用之前執(zhí)行(通過跑出InterruptedException,或者調用isInterrupted和interrupted)
終結器規(guī)則:對象的構造函數必須在啟動該對象的終結器之前執(zhí)行完成。
傳遞性規(guī)則:如果操作A在操作B之前執(zhí)行,并且操作B在操作C之前執(zhí)行,那么操作A必須在操作C之前執(zhí)行。
當程序包含兩個沒有被Happens-Before關系排序的沖突訪問時,就稱存在數據爭用。遵守這個原則,也就意味著有些代碼不能進行重排序。

Java內存模型規(guī)定了所有的變量都存儲在主內存中。每條線程中還有自己的工作內存,線程的工作內存中保存了被該線程所使用到的變量(這些變量是從主內存中拷貝而來)。線程對變量的所有操作(讀取,賦值)都必須在工作內存中進行。不同線程之間也無法直接訪問對方工作內存中的變量,線程間變量值的傳遞均需要通過主內存來完成。
這里所描述的工作內存只是一個概念,實際上只是一個邏輯上的理解,前面所學到的虛擬機棧、CPU高速緩存、程序計數器等等這些我們都可以將它們歸結為線程的工作內存。這里的主內存也不僅僅是堆內存。
一個線程對共享數據的改變能夠實現地反饋到另一個線程中去,我們就說這個共享數據是可見的。
8.2 volatile與可見性
8.2.1 指令重排序
前面說了,如果兩個操作之間缺乏Happens-Before關系,那么JVM可以對它們任意地重排。這里描述的就是指令重排序,Java語言的語意允許編譯器和微處理器執(zhí)行優(yōu)化,而這些優(yōu)化可以與不正確地同步代碼交互,從而產生看似矛盾的行為。

指令重排只能保證單個線程執(zhí)行結果不會出問題,其滿足as-if-serial(前面已有介紹)的規(guī)則。但是多線程情況下,其無法保證最終結果是否會有問題。
回到前面多線程引入的問題中,為什么程序最后一直沒有執(zhí)行完呢,沒錯,就是因為指令的重排序造成最后結果運行不完的問題,下面給出解決方案,請繼續(xù)往下閱讀。
8.2.2 volatile關鍵字與可見性
8.2.2.1 volatile關鍵字的官方描述
下面我們查看官方的Java語言與虛擬機的規(guī)范:https://docs.oracle.com/javase/specs/jls/se8/html/index.html

在Java語言規(guī)范能看到volatile的說明,鏈接如下:https://docs.oracle.com/javase/specs/jls/se8/html/jls-8.html#jls-8.3.1.4
官方文檔中有這么個描述:
The Java programming language provides a second mechanism, volatile fields, that is more convenient than locking for some purposes.
A field may be declared volatile, in which case the Java Memory Model ensures that all threads see a consistent value for the variable
翻譯:Java語言提供第二種機制,volatile域,這種方式在某種目的上比用鎖更方便。一個域如果聲明為volatile,則Java內存模型就能保證所有線程看到這個變量都是一致的。

8.2.3.2 可見性
可見性:讓-個線程對共享變量的修改,能夠及時的被其他線程看到,就說明此變量是滿足可見性的。
根據JMM中規(guī)定的happen before和同步原則:
1)對某個volatile字段的寫操作happens-before每個后續(xù)對該volatile字段的讀操作。
2)對volatile變量v的寫入,與所有其他線程后續(xù)對v的讀同步
?要滿足這些條件,所以volatile關鍵字就有這些功能:
1)禁止緩存:volatile變量的訪問控制符會加個ACC_VOLATILE
官方說明: https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-4.html#jvms-4.5

2)對volatile變量相關的指令不做重排序
8.2.3 JIT即時編譯器
JIT(Just-In-Time Compiler)有三個優(yōu)化級別,它能緩存編譯后的熱點匯編代碼,以及運行一段時間之后可能會改變代碼邏輯,即時編譯器有類似的指令重排的優(yōu)化。所以很可能是因為JIT優(yōu)化后導致結果的不一致問題。我們嘗試關閉JIT優(yōu)化,發(fā)現確實如此。

執(zhí)行效果:發(fā)現程序能夠正常停止

但是JIT優(yōu)化關閉,很可能會導致程序執(zhí)行效率大幅度下降,所以最好還是使用volatile來解決可見性和指令重排的問題。
8.3 JMM的擴展內容
8.3.1 JMM的八種操作
關于主內存與工作內存之間具體的交互協議,即一個變量如何從主內存拷貝到工作內存、如何從工作內存同步回主內存之類的實現細節(jié),Java內存模型中定義了種操作來完成,并且每種操作都是原子的、不可再分的。
lock:作用于主內存的變量,把一個變量標識為一條線程獨占的狀態(tài)
unlock:作用于主內存的變量,把一個處于鎖定狀態(tài)的變量釋放出來。
read:把一個變量的值從主內存?zhèn)鬏數焦ぷ鲀却嬷校员汶S后的load使用。
load:把read操作從主內存中得到的變量值放入到工作內存的變量副本中。
use:把工作內存中一個變量的值傳遞給執(zhí)行引擎,每當虛擬機遇到一個需要使用變量的值的字節(jié)碼指令時將會執(zhí)行這個操作。
assign:把一個從執(zhí)行引擎中接收到的值賦值給工作內存中的變量,每當虛擬機遇到一個給變量賦值的字節(jié)碼指令時執(zhí)行這個操作。
store:把工作內存中的一個變量的值傳遞到主內存,以便隨后的write使用。
write:把store操作從工作內存中得到的變量值放入到主內存的變量中。
8.3.2 同步的規(guī)則定義
1)對于監(jiān)視器m的解鎖與所有后續(xù)操作對于m的加鎖同步
2)對volatile變量v的寫入,與所有其他線程后續(xù)對v的讀同步
3)啟動線程的操作與線程中的第一個操作同步(線程能夠看到操作的數據變化)
4)對于每個屬性寫入默認值(0,false,null)與每個線程對其進行的操作同步
5)線程T1的最后操作于線程T2發(fā)現線程T1已經結束同步(isAlive,join可以判斷線程是否終止)
6)如果線程T1中斷了T2,那么線程T1的中斷操作與其他所有線程發(fā)生T2被中斷了的同步,通過拋出InterruptedException異常,或者調用Thread.interrupted或Thread.isInterrupted。
前面兩條是指導我們寫代碼使用volatile和synchronized關鍵字的一些場景,后面四條是JDK幫我們保證的,我們只要知道就行了。
8.3.3 final在JMM中的處理(大概了解)
1)final在該對象的構造函數中設置對象的字段,當線程看到該對象時,將始終看到該對象的final字段的正確構造版本。
如:f = new finalDemo(),則讀取到的f.x一定是最新的(x為final字段)
2)如果在構造函數中設置字段后發(fā)生讀取,則會看到該final字段分配的值,否則它將看到默認值。
如:public finalDemo(){x=1;y=x;},y會等于1
3)讀取該共享對象的final成員變量之前,先要讀取共享對象
如:r = new ReferenceObj();k=r.f;這兩個操作不能重排序
4)通常static final是不可以修改的字段,然而System.in,System.out和System.err是static final字段,遺留原因,必須允許通過set方法改變,我們將這些字段成為寫保護,以區(qū)別于普通final字段。
class FinalFieldDemo {
final int x;
int y;
public FinalFieldDemo(){
x = 1;
y = 2;
}
}
解釋第一條,意思是多線程下,使用了final字段,去讀取的時候一定能保證x能構造成功,讀取到的值肯定是1,但是y這種普通字段就不能保證,有些線程可能會讀取到默認值0。
8.3.4 字撕裂-double和long的特殊處理(大概了解)
JVM將64位(long和double變量)的讀取和寫入當做兩個分離的32位操作來執(zhí)行,就產生了一個讀取和寫入操作中間發(fā)生上下文切換,從而導致不同的任務可以看到不正確結果的可能性,這種情況叫做字撕裂(word tearing),但是當你定義long、double變量時,使用volatile關鍵字就會獲得(簡單的賦值和返回操作的)原子性(JDK5之前volatile一直沒能正確解決問題),不同的JVM可以任意地提供更強的保證,但是你不應該依賴于平臺相關的特性。