一文帶你深入理解JVM內存模型
一、JAVA的并發模型
共享內存模型
在共享內存的并發模型里面,線程之間共享程序的公共狀態,線程之間通過讀寫內存中公共狀態來進行隱式通信
該內存指的是主內存,實際上是物理內存的一小部分
二、JAVA 內存模型的抽象
1、java內存中哪些數據是線程安全的,哪些是非安全的
非線程安全:
在java中所有的實例域、靜態域、和數組元素都存放在堆內存中,并且這些數據是線程共享的,所以會存在內存可見性問題
線程安全
局部變量、方法定義的參數、異常處理器參數是當前線程的虛擬機棧中的數據,并且不會進行線程共享,所以不會存在內存可見性問題
2、線程間通訊的本質
線程間通訊的本質是
JMM即JAVA內存模型進行控制,JMM決定了一個線程對共享變量的寫入何時對其他線程可見。
由上圖能看出來線程間的通訊都是通過主內存來進行傳遞消息的, 每個線程在進行共享數據處理的時候都是將共享的數據復制到當前線程本地(每個線程自己都有一個內存)來進行操作。
消息通訊過程(不考慮數據安全性的問題)
線程一將主內存中的共享變量 A 加載到自己的本地內存中進行處理。比如 A = 1; 此時將修改的共享變量 A 刷入到主內存中, 之后線程二再將主內存中的共享變量 A 讀取到本地內存進行操作; 整個數據交互的過程是JMM控制的,主要控制主內存與每個線程的本地內存如何進行交互來提供共享數據的可見性
三、重排序
程序在執行的時候為了提高效率會將程序指令進行重新排序
1、重排序分類
編譯器優化重排序
編譯器在不改變單線程程序語義的情況下進行語句執行順序的優化
指令集并行重排序
如果不存在數據的依賴性的話,處理器可以改變語句對應機器指令的執行順序
內存系統重排序
由于處理器使用緩存和讀/寫緩沖區,這使得加載和存儲操作看上去可能是在亂序執行
2、重排序過程
以上三種重排序都會導致我們在寫并發程序的時候出現內存可見性的問題。
JMM的編譯器重排序規則會禁止特定類型的編譯器重排序;
JMM的處理器重排序規則會要求java編譯器在生成指令序列的時候插入特定的內存屏障指令,通過內存屏障指令來禁止特定類型的處理器進行重排序
3、處理器重排序
由于為了避免處理器等待向內存中寫入數據的延時,在處理器和內存中間加了一個緩沖區,這樣處理器可以一直向緩沖區中寫入數據,等到一定時間將緩沖區的數據一次性的刷入到內存中。
優點:
1.處理器不同停頓,提高了處理器的運行效率
2.減少在向內存寫入數據時的內存總線的占用
缺點:
每個處理器上的寫緩沖區只對當前處理器可見,所以就會造成內存操作的執行順序和實際情況不符合 例如以下場景 :
在當前場景中就可能出現在處理器A和處理器B沒有將它們各自的寫緩沖區中的數據刷回內存中, 將內存中讀取的A=0、B =0進行給X和Y賦值,此時將緩沖區的數據刷入內存,導致了最后結果和實際想要的結果不一致。因為只有將緩沖區的數據刷入到了內存中才叫真正的執行
以上主內存與工作內存之間的具體交互協議,即一個變量如何從主內存拷貝到工作內存,如何從工作內存同步到主內存之間的實現細節,JMM定義了以下8種操作來完成
如果要把一個變量從主內存中復制到工作內存中,就需要按順序地執行read和load操作,如果把變量從工作內存中同步到主內存中,就需要按順序地執行store和write操作。但Java內存模型只要求上述操作必須按順序執行,而沒有保證必須是連續執行
操作執行流程圖解:
同步規則分析
- 不允許一個線程無原因地(沒有發生過任何assign操作)把數據從工作內存同步回主內存中
- 一個新的變量只能在主內存中誕生,不允許在工作內存中直接使用一個未被初始化(load或者assign)的變量。即就是對一個變量實施use和store操作之前,必須先自行assign和load操作。
- 一個變量在同一時刻只允許一條線程對其進行lock操作,但lock操作可以被同一線程重復執行多次,多次執行lock后,只有執行相同次數的unlock操作,變量才會被解鎖。lock和unlock必須成對出現。
- 如果對一個變量執行lock操作,將會清空工作內存中此變量的值,在執行引擎使用這個變量之前需要重新執行load或assign操作初始化變量的值。
- 如果一個變量事先沒有被lock操作鎖定,則不允許對它執行unlock操作;也不允許去unlock一個被其他線程鎖定的變量。
- 對一個變量執行unlock操作之前,必須先把此變量同步到主內存中(執行store和write操作)
4、內存屏障指令
為了解決處理器重排序導致的內存錯誤,java編譯器在生成指令序列的適當位置插入內存屏障指令,來禁止特定類型的處理器重排序
內存屏障指令
5、happens-before(先行規則)
happens-before 原則來輔助保證程序執行的原子性、可見性以及有序性的問題,它是判斷數據是否存在競爭、線程是否安全的依據
在JMM中如果一個操作中的結果需要對另一個操作可見,那么這兩個操作之前必須要存在happens-before關系 (兩個操作可以是同一個線程也可以不是一個線程)
規則內容:
程序順序規則
指的是在一個線程內控制代碼順序,比如分支、循環等,即在一個線程內必須保證語義串行性,也就是說按照代碼順序執行
加鎖規則
一個解鎖(unlock)操作一定要發生于一個加鎖(lock)操作之前,也就是說,如果對于一個鎖解鎖后,再加鎖,那么加鎖的動作必須在解鎖動作之后(同一個鎖)
volatile變量規則
對一個volatile的變量的寫操作要發生在對這個變量的讀操作之前,這保證了volatile變量的可見性,簡單的理解就是,volatile變量在每次被線程訪問時,都強迫從主內存中讀該變量的值,而當該變量發生變化時,又會強迫將最新的值刷新到主內存,任何時刻,不同的線程總是能夠看到該變量的最新值
線程啟動規則
線程的啟動方法 start() 要發生在當前線程所有操作之前
線程終止規則
線程中所有的操作都要發生在線程終止之前,Thread.join()方法的作用是等待當前執行的線程終止。假設在線程B終止之前,修改了共享變量,線程A從線程B的join方法成功返回后,線程B對共享變量的修改將對線程A可見
線程中斷規則
線程調用interrupt()方法要發生在被中斷線程的代碼檢查出中斷事件之前
對象終結規則
對象的初始化完成要發生在對象被回收之前
傳遞性規則
如果操作A發生在操作B之前,操作B又發生在操作C之前,那么操作A一定發生于操作C之前
注意:兩個操作之間具有 happens-before 關系,并不意味著前一個操作必須要在后一個操作之前執行,只需要前一個操作的結果對后一個操作可見,并且前一個操作按順序要排在后一個操作之前。
6、數據依賴性
就是前一個操作的結果對后一個操作的結果產生影響,此時編譯器和處理器在處理當前有數據依賴性的操作時不會改變存在數據依賴的兩個操作的執行順序
注意: 此時所說的數據依賴僅僅針對單個處理器中執行的指令序列或者單個線程中執行的操作。不同處理器和不同線程的情況編譯器和處理器是不會考慮的
7、as-if-serial
在單線程情況下不管怎么重排序程序的執行結果不能被改變,所以如果在單處理器或者單線程的情況下,編譯器和處理器對于有數據依賴性的操作是不會進行重排序的。反之如果沒有數據依賴性的操作就有可能發生指令重排。
四、數據競爭與順序一致性
在多線程情況下才會出現數據競爭
1、數據競爭
在一個線程中寫了一個變量,在另一個線程中讀一個變量,而且寫和讀并沒有進行同步
2、順序一致性
如果在多線程條件下,程序能夠正確地使用同步機制,那么程序的執行將具有順序一致性(就像在單線程條件下執行一樣) 程序最終運行的結果與你預期的結果一樣
3、順序一致性內存模型
5.3.1特性:
一個線程中的所有操作必須按照程序的順序來執行 所有的操作都必須是原子性的操作,并且對其他線程可見的
5.3.2概念:
在概念上,順序一致性有一個單一的全局內存,在任意時間點最多只有一個線程可以連接到內存,當在多線程的場景下,會把所有內存的讀寫操作變成串行化
5.3.3案例:
例如有多個并發線程A B C, A 線程有兩個操作A1 A2, 他們的執行的順序是 A1->A2 。B 線程有三個操作B1 B2 B3, 他們的執行的順序是B1->B2->B3 。C線程有兩個操作C1 C2那么他們在程序中執行的順序是C1->C2 。
場景分析:
場景一: 并發安全(同步)執行順序
A1->A2->B1->B2->B3->C1->C2
場景二: 并發不安全(非同步)執行順序
A1->B1->A2->C1->B2->B3->C2
結論:
在非同步的場景下,即使三個線程中的每一個操作亂序執行,但是在每個線程中的各自操作還是保持有序的。并且所有線程都只能看到一個一致的整體執行順序,也就是說三個線程看到的都是該順序 : A1->B1->A2->C1->B2->B3->C2 ,因為順序一致性內存模型中的每個操作必須立即對任意線程可見。
以上案例場景在JMM中不是這樣的,未同步的程序在JMM中不僅整體的執行順序變了,就連每個線程的看到的操作執行順序也是不一樣的。
例如前面所說的如果線程A將變量的值a=2寫入到了自己的本地內存中,還沒有刷入到主存中,在線程 A 來看值是變了,但是其他線程B線程C根本看不到值得改變,就認為線程A的操作還沒有發生,只有線程A將工作內存中的值刷回主內存線程B和線程C才能的到。但是如果是同步的情況下,順序一致性模型和JMM模型執行的結果是一致的,但是程序的執行順序不一定,因為在JMM中,會發生指令重排現象所以執行順序會不一致。