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