JMM内存模型如何为并发保驾护航
一.為何引入JMM
每個處理器在執(zhí)行任務時,不可能單靠"計算"就可以完成所有任務,處理器至少需要和內(nèi)存交互,進行讀取運算數(shù)據(jù)、存儲運算結(jié)果等,這個I/O操作是很難消除掉的。但由于計算機的存儲設備與處理器的運算速度之間相差了幾個數(shù)量級的差距,所以現(xiàn)代計算機系統(tǒng)都不得不加入一層讀寫速度盡可能與處理器運算速度相近的高速緩存(Cache),作為內(nèi)存預處理器之間的緩沖,將運算需要使用到的數(shù)據(jù)復制到緩存中,讓運算能快速進行,當運算結(jié)束后再從緩存同步回內(nèi)存中,這樣處理器就無需等待緩慢的內(nèi)存讀寫了。
但是引入緩存給計算機帶來了新的問題:緩存一致性問題。在目前已經(jīng)很普遍的多處理器計算機中,每個處理器都有著自己的高速緩存,而他們又共享一個主內(nèi)存,當多個處理器的運算任務都涉及到同一塊內(nèi)存時,就會導致各自的緩存數(shù)據(jù)不一致。
為了解決緩存一致性問題,需要各個處理器訪問緩存時遵守一些協(xié)議,比如MESI協(xié)議等。
以上是引入Java內(nèi)存模型的預置條件。
Java虛擬機規(guī)范試圖定義一種Java內(nèi)存模型JMM,來屏蔽各種硬件和操作系統(tǒng)的內(nèi)存訪問差異,以實現(xiàn)讓Java程序在各種平臺下都能達到一致的內(nèi)存訪問效果,真正做到:“Code once,run everywhere”。這個模型必須足夠嚴謹,讓Java并發(fā)內(nèi)存訪問操作不會有歧義,也必須做到足夠?qū)捤?#xff0c;使得虛擬機的實現(xiàn)有足夠多自由空間去利用硬件的各種特性來獲取更好的執(zhí)行速度。
Java內(nèi)存模型對并發(fā)的保證主要在于實現(xiàn)原子性,可見性,和有序性三大特性。
二.JMM內(nèi)存模型
并發(fā)編程中,有兩個關(guān)鍵問題
- 線程之間如何通信
- 線程之間如何同步
線程之間的通信機制有兩種:共享內(nèi)存和消息傳遞
共享內(nèi)存
在共享內(nèi)存的并發(fā)模型中,線程之間共享程序的公共部分,程序必須顯式規(guī)定某些指令必須在線程之間互斥執(zhí)行,這時同步是顯式實現(xiàn)的,而消息傳遞是通過共享的內(nèi)存隱式實現(xiàn)的。
消息傳遞
在消息傳遞的并發(fā)模型中,線程之間必須發(fā)送消息來顯式進行通信,但是消息接收方拿到資源必然在消息發(fā)送方發(fā)送之后,因此同步時隱式實現(xiàn)的。
打個比方,程序員A要和程序員B聯(lián)手開發(fā)一個項目,他們有沒有什么經(jīng)驗,相互實現(xiàn)的部分耦合度很高,A要想和B合作,要么用一臺電腦,A寫代碼的時候B就在那瞧,硬瞧,B寫的時候A就在那瞧,這是共享內(nèi)存方式,還有一種方式就是A寫了一部分,通過網(wǎng)絡發(fā)給B,B收到消息,拿到代碼接著寫,這就是消息傳遞方式。
如何AB之間的交流出現(xiàn)問題,最后就會導致版本混亂,也就是線程安全問題。
Java中,線程間的通信是通過共享內(nèi)存來完成的。JMM就規(guī)定不同線程對變量的所有操作都必須在本地內(nèi)存中進行,而不能直接讀寫主內(nèi)存中的共享變量,線程之間的傳遞均需要JMM控制并通過主內(nèi)存完成。
在Java中,所有實例域、靜態(tài)域、和數(shù)組元素都存儲在堆中,而堆線程間共享的。局部變量,方法定義參數(shù),異常處理參數(shù)這些都存儲在虛擬機棧中,不會在線程之間共享。
JMM抽象結(jié)構(gòu)示意圖:
JMM定義了線程和主內(nèi)存之間的抽象關(guān)系,線程之間的共享變量存儲在主內(nèi)存中,每個線程都有本地內(nèi)存,存儲著該線程讀/寫的共享變量副本。
本地內(nèi)存是JMM抽象的一個概念,本身并不存在,它涵蓋了緩存,寫緩沖區(qū),寄存器以及其他硬件和編譯器優(yōu)化。
CPU 與 Cache 結(jié)構(gòu)圖:
假設線程A、B之間要進行通信,需要經(jīng)歷一下步驟
- 線程A把本地內(nèi)存更新過的共享變量副本刷新到主內(nèi)存中去
- 線程B到主內(nèi)存中去讀取線程A之前已更新過的共享變量
三. 原子性
1.JMM對原子性的保證
JMM中定義了以下8種原子性操作,虛擬機實現(xiàn)時必須保證其中的每一種都是原子的,不可再分的,這八種原子操作建立在MESI協(xié)議基礎上。
- lock(鎖定):
作用于主內(nèi)存的變量,把一個變量標識為一條線程獨占的狀態(tài) - unlock(解鎖):
作用于主內(nèi)存的變量,它把一個處于鎖定狀態(tài)的變量釋放出來,釋放后的變量才可以被其他線程鎖定 - read(讀取)
作用于主內(nèi)存的變量,它把一個變量的值從主內(nèi)存?zhèn)鬏數(shù)骄€程的工作內(nèi)存中,以便于隨后的load動作讀取 - load(載入)
作用于工作內(nèi)存的變量,它把read操作從主內(nèi)存得到的變量值放入工作內(nèi)存中的變量副本中 - use(使用)
作用于工作內(nèi)存的變量,它把工作內(nèi)存中的一個變量值傳給執(zhí)行引擎,每當虛擬機遇到一個需要使用到變量的字節(jié)碼指令時將會執(zhí)行這個操作 - assign(賦值)
作用域工作內(nèi)存的變量,它把一個從執(zhí)行引擎接收到的值賦給工作內(nèi)存變量,每當虛擬機遇到一個變量賦值的字節(jié)碼指定時執(zhí)行這個操作。 - store(存儲)
作用域工作內(nèi)存的變量,它把工作內(nèi)存中的一個變量的值傳送到主內(nèi)存中,以便隨后的write操作時使用 - write(寫入)
作用于主內(nèi)存的變量,它把store操作從工作內(nèi)存中得到的變量的值放入到主內(nèi)存的變量中。
Java內(nèi)存模型還規(guī)定了執(zhí)行上述8種基本操作時必須滿足如下規(guī)則:
- 不允許read和load、store和write操作之一單獨出現(xiàn)(即不允許一個變量從主存讀取了但是工作內(nèi)存不接受,或者從工作內(nèi)存發(fā)起會寫了但是主存不接受的情況),以上兩個操作必須按順序執(zhí)行,但沒有保證必須連續(xù)執(zhí)行,也就是說,read與load之間、store與write之間是可插入其他指令的。
- 不允許一個線程丟棄它的最近的assign操作,即變量在工作內(nèi)存中改變了之后必須把該變化同步回主內(nèi)存。
- 不允許一個線程無原因地(沒有發(fā)生過任何assign操作)把數(shù)據(jù)從線程的工作內(nèi)存同步回主內(nèi)存中。
- 一個新的變量只能從主內(nèi)存中“誕生”,不允許在工作內(nèi)存中直接使用一個未被初始化(load或assign)的變量,換句話說就是對一個變量實施use和store操作之前,必須先執(zhí)行過了assign和load操作。
- 一個變量在同一個時刻只允許一條線程對其執(zhí)行l(wèi)ock操作,但lock操作可以被同一個條線程重復執(zhí)行多次,多次執(zhí)行l(wèi)ock后,只有執(zhí)行相同次數(shù)的unlock操作,變量才會被解鎖。
- 如果對一個變量執(zhí)行l(wèi)ock操作,將會清空工作內(nèi)存中此變量的值,在執(zhí)行引擎使用這個變量前,需要重新執(zhí)行l(wèi)oad或assign操作初始化變量的值。
- 如果一個變量實現(xiàn)沒有被lock操作鎖定,則不允許對它執(zhí)行unlock操作,也不允許去unlock一個被其他線程鎖定的變量。
- 對一個變量執(zhí)行unlock操作之前,必須先把此變量同步回主內(nèi)存(執(zhí)行store和write操作)。
其中read,load,use,assign,store,write主要保證基本數(shù)據(jù)類型訪問讀寫操作的原子性,而lock以及unlock則實現(xiàn)了更廣范圍上的原子性保證。
2.并發(fā)編程中的具體體現(xiàn)
基本數(shù)據(jù)類型訪問讀寫操作的原子性是JMM為我們提供的最低保證,我們在并發(fā)編程不用考慮這些,lock,unlock操作也沒有直接開放給用戶,但是提供了字節(jié)碼指令monitorenter,monitorexit來隱式使用這兩個操作,這兩個字節(jié)碼對應到Java代碼就是synchronizd關(guān)鍵字,具體關(guān)于這個關(guān)鍵字的使用我會在其他博客中總結(jié)一下,在此不詳細討論。
三.有序性
1.JMM對有序性的實現(xiàn)
在執(zhí)行程序時,為了提高性能,編譯器和處理器常常會對指令做重排序,但是有些重排序勢必會導致程序執(zhí)行結(jié)果發(fā)生變化,JMM是通過限制編譯器和處理器的重排序功能來保證順序一致性與可見性
首先來看重排序本身,重排序分三種類型:
編譯器優(yōu)化的重排序
編譯器在不改變單線程程序語義的前提下,可以重新安排語義的執(zhí)行順序。
指令級并行的重排序
現(xiàn)代處理器采用了指令級并行技術(shù)(ILP),來將多條指令重疊執(zhí)行,如果不存在數(shù)據(jù)依賴性,處理器可以改變語句對應機器指令的執(zhí)行順序。
內(nèi)存系統(tǒng)的重排序
由于處理器使用緩存和讀/寫緩沖區(qū),這使得加載和存儲操作看上去可能是在亂序執(zhí)行。
一個程序可能經(jīng)過的重排序過程
上述的第一種屬于編譯器重排序,第二種和第三種屬于處理器重排序。
JMM對于編譯器重排序,JMM的編譯器重排序格則將直接禁止某些特定類型的編譯器重排序。
JMM對于處理器重排序,JMM的處理器重排序規(guī)則會要求Java編譯器在生成指令序列時,插入特定類型的內(nèi)存屏障指令,通過內(nèi)存屏障來禁止特定類型的處理器重排序。
內(nèi)存屏障類型表:
| LoadLoad Barriers | Load1;loadload;Load2 | 確保Load1數(shù)據(jù)的裝載完成后,才能執(zhí)行Load2以及所有后續(xù)裝載指令的裝載 |
| StoreStore Barriers | Store1;storestore;Store2 | 確保Store1數(shù)據(jù)對其他處理器可見(刷新到內(nèi)存)完成后,才能執(zhí)行Store2及所有后續(xù)存儲指令的存儲 |
| LoadStore Barriers | Load1;loadstore;Store2 | 確保Load1數(shù)據(jù)裝載完成后,才能執(zhí)行Store2及所有后續(xù)的存儲指令刷新到內(nèi)存 |
| StoreLoad Barriers | Store1;storeload;Load2 | 確保Store1數(shù)據(jù)對其他處理器可見(刷新到內(nèi)存)完成后,才能執(zhí)行Load2及所有后續(xù)裝載指令的裝載。StoreLoad Barriers會使該屏障之前的所有內(nèi)存訪問指令完成之后,才執(zhí)行屏障之后的內(nèi)存訪問指令 |
關(guān)于Load屏障:屏障之前的操作執(zhí)行先于屏障之后的操作,而且后續(xù)的操作必須先等當前處理器裝載操作完成,不能我讀的時候讓其他處理器又給更改了,保證當前處理器的讀對其他處理器是可見的。
關(guān)于Store屏障:屏障之前的操作執(zhí)行先于屏障之后的操作,而且后續(xù)的操作必須等我把當前處理器的本地緩存數(shù)據(jù)刷新到主內(nèi)存中,不能我正在刷新的時候其他處理器去讀舊數(shù)據(jù),保證當前處理器的寫對其他處理器是可見的。
以上四種屏障StoreLoad Barriers是全能的,但是它的開銷非常大,它要求當前處理器把屏障前涉及的所有緩存區(qū)數(shù)據(jù)全部刷新到內(nèi)存中。
所以真正意義上,不是內(nèi)存屏障真的禁止了處理器重排序,而是處理器重排序無法越過屏障,屏障前的操作始終先于屏障后的操作。
2.并發(fā)編程中的具體體現(xiàn)
在實際的并發(fā)編程中,我們使用volatile關(guān)鍵字和synchronized關(guān)鍵字來實現(xiàn)有序性保證。
四.可見性
JMM通過volatile,final和鎖的內(nèi)存語義保證可見性。
1.volatile內(nèi)存語義
volatile自身具有以下特性:
- 可見性。對一個volatile變量的讀,總是能看到任意線程對這個volatile變量最后的寫入
- 原子性。對任意單個volatile變量的讀/寫本身具有原子性,但是類似于volatile++這種復合操作不具有原子性
volatile寫的內(nèi)存語義:
當寫一個volatile變量時,JMM會把該線程對應的本地內(nèi)存中的共享變量刷新到主內(nèi)存中
volatile讀的內(nèi)存語義:
當讀一個volatile變量時,JMM會把該線程的對應的本地內(nèi)存置為無效,從主內(nèi)存中讀取共享變量
如何實現(xiàn)的?
JMM對此采取的時保守策略,具體策略如下:
- 在每個volatile寫操作的前面插入一個StoreStore屏障
- 在每個volatile寫操作的后面插入一個StoreLoad屏障
- 在每個volatile讀操作的后面插入一個LoadLoad屏障
- 在每個volatile讀操作的后面插入一個LoadStore屏障
2.鎖的內(nèi)存語義
眾所周知,鎖可以讓臨界區(qū)互斥執(zhí)行,每次只允許一個線程訪問,保證可見性與原子性
獲取鎖的內(nèi)存語義:
當線程獲取鎖時,JMM會把線程對應的本地內(nèi)存置為無效。從而使得被監(jiān)視器保護的臨界代碼必須從主內(nèi)存中讀取內(nèi)存變量。
釋放鎖的內(nèi)存語義:
當線程釋放鎖時,JMM會把線程對應的本地內(nèi)存中的共享變量刷新到主內(nèi)存中。
如何實現(xiàn)?
利用synchronized關(guān)鍵字或者其他互斥鎖
利用CAS的非阻塞輕量鎖
五.JMM的happens-before原則
happens-before原則是JMM對程序員的保證,讓程序員們安心寫代碼,不用擔心因為重排序或其他非主管因素導致自己程序異常(所以你的并發(fā)程序出了問題,別想甩鍋,不關(guān)人家編譯器和處理器的事)
JMM與happens-before原則之間的關(guān)系如圖所示:
《JSR-133》對happens-before原則的定義:
happens-befores規(guī)則:
- 程序次序規(guī)則:一個線程內(nèi),按照代碼順序,書寫在前面的操作先行發(fā)生于書寫在后面的操作;
- 鎖定規(guī)則:一個unLock()操作先行發(fā)生于后面對同一個鎖的lock()操作;
- volatile變量規(guī)則:對一個變量的寫操作先行發(fā)生于后面對這個變量的讀操作;
- 傳遞規(guī)則:如果操作A先行發(fā)生于操作B,而操作B又先行發(fā)生于操作C,則可以得出操作A先行發(fā)生于操作C;
- start()規(guī)則:Thread對象的start()方法先行發(fā)生于此線程的每個一個動作;
- interrupt()規(guī)則:對線程interrupt()方法的調(diào)用先行發(fā)生于被中斷線程的代碼檢測到中斷事件的發(fā)生;
- join()規(guī)則:線程中所有的操作都先行發(fā)生于線程的終止檢測,我們可以通過Thread.join()方法結(jié)束、Thread.isAlive()的返回值手段檢測到線程已經(jīng)終止執(zhí)行;
總結(jié)
以上是生活随笔為你收集整理的JMM内存模型如何为并发保驾护航的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 从JVM看类的加载过程与对象实例化过程
- 下一篇: 你必须会的启发式搜索算法--A*算法