多線程之Java內(nèi)存模型(JMM)(一)

在未正確使用鎖的時候,多線程的程序可能變的很容易出錯,并且難以排查。而JMM則給我們一種規(guī)范,它描述了多線程程序如何與內(nèi)存交互。


與文無關(guān)

JMM大致描述:

  • JMM描述了線程如何與內(nèi)存進(jìn)行交互。Java虛擬機(jī)規(guī)范視圖定義一種Java內(nèi)存模型,來屏蔽掉各種操作系統(tǒng)內(nèi)存訪問的差異,以實(shí)現(xiàn)Java程序在各種平臺下都能達(dá)到一致的訪問效果。
  • JMM描述了JVM如何與計算機(jī)的內(nèi)存進(jìn)行交互
  • JMM都是圍繞著原子性,有序性和可見性進(jìn)行展開的

JMM的主要目標(biāo)是定義程序中各個變量的訪問規(guī)則,虛擬機(jī)將變量存儲到內(nèi)存和從內(nèi)存取出變量這樣的底層細(xì)節(jié)。此處的變量指在堆中存儲的元素。

多線程的時候為什么容易出錯?

Java內(nèi)存模型規(guī)定所有的共享變量都存儲在主內(nèi)存中,而每條線程有自己的工作內(nèi)存(本地內(nèi)存),工作內(nèi)存保存了共享變量的副本,而不同內(nèi)存又無法訪問對方的工作內(nèi)存,所以如果線程在工作內(nèi)存中修改了變量副本,其它線程是無從得知的。

線程的傳值均需要通過主內(nèi)存來完成

JMM模型

JMM模型

主內(nèi)存與工作內(nèi)存如何交互?

Java內(nèi)存模型定義了8種操作來完成主內(nèi)存與工作內(nèi)存的交互細(xì)節(jié),虛擬機(jī)必須保證這8種操作的每一個操作都是原子的,不可再分的。

  • lock: 作用于主內(nèi)存的變量,把變量標(biāo)識為線程獨(dú)占的狀態(tài)
  • unlock: 與lock對應(yīng),把主內(nèi)存中處于鎖定狀態(tài)的變量釋放出來,釋放后的變量才可以被其他線程鎖定。
  • read: 作用于主內(nèi)存的變量,把一個變量的值從主內(nèi)存?zhèn)鬏數(shù)骄€程的工作內(nèi)存,便于隨后的load使用。
  • load:作用于工作內(nèi)存的變量,把read讀取到的變量放入工作內(nèi)存副本
  • use: 作用于工作內(nèi)存,把工作內(nèi)存的變量值傳遞給執(zhí)行引擎,每當(dāng)虛擬機(jī)遇到一個需要使用到變量的值的字節(jié)碼指令時將會執(zhí)行這個操作。
  • assign: 作用于工作內(nèi)存,把執(zhí)行引擎收到的值賦給工作內(nèi)存的變量,虛擬機(jī)遇到賦值字節(jié)碼時候執(zhí)行這個操作
  • store:作用于工作內(nèi)存,把變量的值傳輸?shù)阶?nèi)存中,以便隨后的write使用
  • write:作用于主內(nèi)存,把store操作從工作內(nèi)存得到的值放入主內(nèi)存的變量中。
JMM內(nèi)存模型

執(zhí)行上述8種基本操作的規(guī)則:

  • 不允許read和load,store和write操作之一單獨(dú)出現(xiàn)。
  • 不允許一個線程丟棄它最近的assign操作。即變量在工作內(nèi)存中改變了賬號必須把變化同步回主內(nèi)存
  • 一個新的變量只允許在主內(nèi)存中誕生,不允許工作內(nèi)存直接使用未初始化的變量。
  • 一個變量同一時刻只允許一條線程進(jìn)行l(wèi)ock操作,但同一線程可以lock多次,lock多次之后必須執(zhí)行同樣次數(shù)的unlock操作
  • 如果對一個變量進(jìn)行l(wèi)ock操作,那么將會清空工作內(nèi)存中此變量的值。
  • 不允許對未lock的變量進(jìn)行unlock操作,也不允許unlock一個被其它線程lock的變量
  • 如果一個變量執(zhí)行unlock操作,必須先把次變了同步回主內(nèi)存中。

這8種操作定義相當(dāng)嚴(yán)禁,實(shí)踐起來又比較麻煩,但是可以有助于我們理解多線程的工作原理。有一個與此8種操作相等的Happen-before原則。

Happen-before原則

這個是Java內(nèi)存模型下無需任何同步器協(xié)助就已經(jīng)存在,可以直接在編碼中使用。如果兩個操作之間的關(guān)系不在此列,并且無法從下列規(guī)則推導(dǎo)出來的話,它們的順序就沒有保障,虛擬機(jī)可以對他們進(jìn)行任意的重排。

天然的happen-before

  • 程序順序原則:一個線程內(nèi)包裝語義的串行性
  • volatile變量的寫,先發(fā)生于讀,這保證了volatile變量的可見性
  • 鎖規(guī)則:unlock先與lock
  • 傳遞性:A 先于B,B先于C,那么A必然先于C
  • 線程的start先于線程的每一個動作
  • 線程的所有操作優(yōu)先于線程的終結(jié)(Thread.join())
  • 線程的中斷(interupt)先于被中斷線程的代碼
  • 對象的構(gòu)造函數(shù)執(zhí)行,先于finalize()方法

Java運(yùn)行時數(shù)據(jù)區(qū)

JVM定義了一些程序運(yùn)行時會使用到的運(yùn)行時數(shù)據(jù)區(qū),其中一些會隨著虛擬機(jī)啟動而創(chuàng)建,隨著虛擬機(jī)退出而銷毀。另外一些是與現(xiàn)場一一對應(yīng)的,這些線程對應(yīng)的數(shù)據(jù)區(qū)會隨著線程的開始和結(jié)束而創(chuàng)建和銷毀。
這部分參考JVM規(guī)范

1. pc寄存器

可以支持多條線程同時允許,每一條Java虛擬機(jī)線程都有自己的pc寄存器。任意時刻,一條JVM線程之后執(zhí)行一個方法的代碼,這個方法被稱為當(dāng)前方法(current method)
如果這個方法不是native的,那么PC寄存器就保存JVM正在執(zhí)行的字節(jié)碼指令地址。
如果是native的,那么pc寄存器的值為undefined
pc寄存器的容量至少能保證一個returnAddress類型的數(shù)據(jù)或者一個平臺無關(guān)的本地指針的值。

2. JVM Stack(虛擬機(jī)棧)
  • 每一個JVM線程都有自己的私有虛擬機(jī)棧,這個棧與線程同時創(chuàng)建,用于存儲棧幀(Frame)。
  • 棧用來存儲局部變量與一些過程結(jié)果的地方。在方法調(diào)用和返回中也扮演了很重要的角色。
  • ??梢栽嚬潭ǚ峙涞囊部梢詣討B(tài)調(diào)整
    • 如果請求線程分配的容量超過JVM棧允許的最大容量,拋出StackOverflowError異常
    • 如果JVM棧可以動態(tài)擴(kuò)展,擴(kuò)展的動作也已經(jīng)嘗試過,但是沒有申請到足夠的內(nèi)存,則拋出OutofMemoryError異常
3. Heap(堆)

堆是可以可供各個線程共享的運(yùn)行時存儲區(qū)域,也是供所有類的實(shí)例和數(shù)組對象分配內(nèi)存的區(qū)域。堆在JVM啟動的時候創(chuàng)建。
堆所存儲的就是被GC所管理的各種對象。
堆也是可以固定大小和動態(tài)調(diào)整的:
實(shí)際所需的堆超過的GC所提供的最大容量,那么JVM拋出OutofMemoryError異常。

4. Method Area(方法區(qū))

也是各個線程共享的運(yùn)行時內(nèi)存區(qū),它存儲每一個類的實(shí)例信息,運(yùn)行時常量池,字段和方法數(shù)據(jù),構(gòu)造函數(shù)和普通方法的字節(jié)碼等內(nèi)容。還有一些特殊方法。

方法區(qū)是堆的邏輯組成部分,也在JVM啟動時創(chuàng)建,簡單的JVM可以不實(shí)現(xiàn)這個區(qū)域的垃圾收集。

方法區(qū)也可固定大小和動態(tài)分配與堆一樣,內(nèi)存空間不夠,那么JVM拋出OutofMemoryError異常。

5. Run-Time Constant Pool(運(yùn)行時常量池)

在方法區(qū)中分配,在加載類和接口到虛擬機(jī)之后,就創(chuàng)建對應(yīng)的運(yùn)行時常量池。

它是class文件中每一個類或接口的常量池表的運(yùn)行時表現(xiàn)形式。像字符串。Java的主要類型。

存儲區(qū)域不夠用時候拋出OutofMemoryError異常。

6. Native Method Stacks(原生方法棧或本地方法棧)

JDK中native的方法,System類和Thread類中有很多。使用C語言編寫的方法,這個也通常叫做C stack。

可以不支持本地方法棧,但是如果支持的時候,這個棧一般會在線程創(chuàng)建的時候按線程分配。

與棧的錯誤一樣,StackOverFlowError和OutOfMemeoryError.

一個案例
案例
  • 一個本地變量可能是原始類型,在這種情況下,它總是“呆在”線程棧上。
  • 一個本地變量也可能是指向一個對象的一個引用。在這種情況下,引用(這個本地變量)存放在線程棧上,但是對象本身存放在堆上。
  • 一個對象可能包含方法,這些方法可能包含本地變量。這些本地變量任然存放在線程棧上,即使這些方法所屬的對象存放在堆上。
  • 一個對象的成員變量可能隨著這個對象自身存放在堆上。不管這個成員變量是原始類型還是引用類型。
  • 靜態(tài)成員變量跟隨著類定義一起也存放在堆上。
  • 存放在堆上的對象可以被所有持有對這個對象引用的線程訪問。當(dāng)一個線程可以訪問一個對象時,它也可以訪問這個對象的成員變量。如果兩個線程同時調(diào)用同一個對象上的同一個方法,它們將會都訪問這個對象的成員變量,但是每一個線程都擁有這個本地變量的私有拷貝。
Java內(nèi)存模型和硬件內(nèi)存架構(gòu)之間的對應(yīng)

最后

這次主要講了一些規(guī)則相關(guān)的東西,及Java中運(yùn)行時數(shù)據(jù)存儲的位置,建議看一下《深入理解JVM》最后一章。最好下載JSR-133規(guī)范對照著看。

參考:

  • Java內(nèi)存模型
  • 《深入理解Java虛擬機(jī)》
  • 《Java高并發(fā)程序設(shè)計》
  • 《JVM specification》
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容