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