日韩av黄I国产麻豆传媒I国产91av视频在线观看I日韩一区二区三区在线看I美女国产在线I麻豆视频国产在线观看I成人黄色短片

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 >

JMM内存模型如何为并发保驾护航

發布時間:2025/6/17 29 豆豆
生活随笔 收集整理的這篇文章主要介紹了 JMM内存模型如何为并发保驾护航 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

一.為何引入JMM

每個處理器在執行任務時,不可能單靠"計算"就可以完成所有任務,處理器至少需要和內存交互,進行讀取運算數據、存儲運算結果等,這個I/O操作是很難消除掉的。但由于計算機的存儲設備與處理器的運算速度之間相差了幾個數量級的差距,所以現代計算機系統都不得不加入一層讀寫速度盡可能與處理器運算速度相近的高速緩存(Cache),作為內存預處理器之間的緩沖,將運算需要使用到的數據復制到緩存中,讓運算能快速進行,當運算結束后再從緩存同步回內存中,這樣處理器就無需等待緩慢的內存讀寫了。

但是引入緩存給計算機帶來了新的問題:緩存一致性問題。在目前已經很普遍的多處理器計算機中,每個處理器都有著自己的高速緩存,而他們又共享一個主內存,當多個處理器的運算任務都涉及到同一塊內存時,就會導致各自的緩存數據不一致。

為了解決緩存一致性問題,需要各個處理器訪問緩存時遵守一些協議,比如MESI協議等。

以上是引入Java內存模型的預置條件

Java虛擬機規范試圖定義一種Java內存模型JMM,來屏蔽各種硬件和操作系統的內存訪問差異,以實現讓Java程序在各種平臺下都能達到一致的內存訪問效果,真正做到:“Code once,run everywhere”。這個模型必須足夠嚴謹,讓Java并發內存訪問操作不會有歧義,也必須做到足夠寬松,使得虛擬機的實現有足夠多自由空間去利用硬件的各種特性來獲取更好的執行速度。

Java內存模型對并發的保證主要在于實現原子性可見性,和有序性三大特性。

二.JMM內存模型

并發編程中,有兩個關鍵問題

  • 線程之間如何通信
  • 線程之間如何同步

線程之間的通信機制有兩種:共享內存和消息傳遞

  • 共享內存
    在共享內存的并發模型中,線程之間共享程序的公共部分,程序必須顯式規定某些指令必須在線程之間互斥執行,這時同步是顯式實現的,而消息傳遞是通過共享的內存隱式實現的。

  • 消息傳遞
    在消息傳遞的并發模型中,線程之間必須發送消息來顯式進行通信,但是消息接收方拿到資源必然在消息發送方發送之后,因此同步時隱式實現的。

  • 打個比方,程序員A要和程序員B聯手開發一個項目,他們有沒有什么經驗,相互實現的部分耦合度很高,A要想和B合作,要么用一臺電腦,A寫代碼的時候B就在那瞧,硬瞧,B寫的時候A就在那瞧,這是共享內存方式,還有一種方式就是A寫了一部分,通過網絡發給B,B收到消息,拿到代碼接著寫,這就是消息傳遞方式。
    如何AB之間的交流出現問題,最后就會導致版本混亂,也就是線程安全問題。

    Java中,線程間的通信是通過共享內存來完成的。JMM就規定不同線程對變量的所有操作都必須在本地內存中進行,而不能直接讀寫主內存中的共享變量,線程之間的傳遞均需要JMM控制并通過主內存完成。

    在Java中,所有實例域、靜態域、和數組元素都存儲在堆中,而堆線程間共享的。局部變量,方法定義參數,異常處理參數這些都存儲在虛擬機棧中,不會在線程之間共享。

    JMM抽象結構示意圖:

    JMM定義了線程和主內存之間的抽象關系,線程之間的共享變量存儲在主內存中,每個線程都有本地內存,存儲著該線程讀/寫的共享變量副本。
    本地內存是JMM抽象的一個概念,本身并不存在,它涵蓋了緩存,寫緩沖區,寄存器以及其他硬件和編譯器優化。

    CPU 與 Cache 結構圖:



    假設線程A、B之間要進行通信,需要經歷一下步驟

    • 線程A把本地內存更新過的共享變量副本刷新到主內存中去
    • 線程B到主內存中去讀取線程A之前已更新過的共享變量

    三. 原子性

    1.JMM對原子性的保證

    JMM中定義了以下8種原子性操作,虛擬機實現時必須保證其中的每一種都是原子的,不可再分的,這八種原子操作建立在MESI協議基礎上。

    • lock(鎖定):
      作用于主內存的變量,把一個變量標識為一條線程獨占的狀態
    • unlock(解鎖):
      作用于主內存的變量,它把一個處于鎖定狀態的變量釋放出來,釋放后的變量才可以被其他線程鎖定
    • read(讀取)
      作用于主內存的變量,它把一個變量的值從主內存傳輸到線程的工作內存中,以便于隨后的load動作讀取
    • load(載入)
      作用于工作內存的變量,它把read操作從主內存得到的變量值放入工作內存中的變量副本中
    • use(使用)
      作用于工作內存的變量,它把工作內存中的一個變量值傳給執行引擎,每當虛擬機遇到一個需要使用到變量的字節碼指令時將會執行這個操作
    • assign(賦值)
      作用域工作內存的變量,它把一個從執行引擎接收到的值賦給工作內存變量,每當虛擬機遇到一個變量賦值的字節碼指定時執行這個操作。
    • store(存儲)
      作用域工作內存的變量,它把工作內存中的一個變量的值傳送到主內存中,以便隨后的write操作時使用
    • write(寫入)
      作用于主內存的變量,它把store操作從工作內存中得到的變量的值放入到主內存的變量中。

    Java內存模型還規定了執行上述8種基本操作時必須滿足如下規則:

    • 不允許read和load、store和write操作之一單獨出現(即不允許一個變量從主存讀取了但是工作內存不接受,或者從工作內存發起會寫了但是主存不接受的情況),以上兩個操作必須按順序執行,但沒有保證必須連續執行,也就是說,read與load之間、store與write之間是可插入其他指令的。
    • 不允許一個線程丟棄它的最近的assign操作,即變量在工作內存中改變了之后必須把該變化同步回主內存。
    • 不允許一個線程無原因地(沒有發生過任何assign操作)把數據從線程的工作內存同步回主內存中。
    • 一個新的變量只能從主內存中“誕生”,不允許在工作內存中直接使用一個未被初始化(load或assign)的變量,換句話說就是對一個變量實施use和store操作之前,必須先執行過了assign和load操作。
    • 一個變量在同一個時刻只允許一條線程對其執行lock操作,但lock操作可以被同一個條線程重復執行多次,多次執行lock后,只有執行相同次數的unlock操作,變量才會被解鎖。
    • 如果對一個變量執行lock操作,將會清空工作內存中此變量的值,在執行引擎使用這個變量前,需要重新執行load或assign操作初始化變量的值。
    • 如果一個變量實現沒有被lock操作鎖定,則不允許對它執行unlock操作,也不允許去unlock一個被其他線程鎖定的變量。
    • 對一個變量執行unlock操作之前,必須先把此變量同步回主內存(執行store和write操作)。

    其中readloaduseassignstorewrite主要保證基本數據類型訪問讀寫操作的原子性,而lock以及unlock則實現了更廣范圍上的原子性保證。

    2.并發編程中的具體體現

    基本數據類型訪問讀寫操作的原子性是JMM為我們提供的最低保證,我們在并發編程不用考慮這些,lockunlock操作也沒有直接開放給用戶,但是提供了字節碼指令monitorenter,monitorexit來隱式使用這兩個操作,這兩個字節碼對應到Java代碼就是synchronizd關鍵字,具體關于這個關鍵字的使用我會在其他博客中總結一下,在此不詳細討論。

    三.有序性

    1.JMM對有序性的實現

    在執行程序時,為了提高性能,編譯器和處理器常常會對指令做重排序,但是有些重排序勢必會導致程序執行結果發生變化,JMM是通過限制編譯器和處理器的重排序功能來保證順序一致性與可見性

    首先來看重排序本身,重排序分三種類型:

  • 編譯器優化的重排序

    編譯器在不改變單線程程序語義的前提下,可以重新安排語義的執行順序。

  • 指令級并行的重排序

    現代處理器采用了指令級并行技術(ILP),來將多條指令重疊執行,如果不存在數據依賴性,處理器可以改變語句對應機器指令的執行順序。

  • 內存系統的重排序

    由于處理器使用緩存和讀/寫緩沖區,這使得加載和存儲操作看上去可能是在亂序執行。

  • 一個程序可能經過的重排序過程

    上述的第一種屬于編譯器重排序,第二種和第三種屬于處理器重排序

    JMM對于編譯器重排序,JMM的編譯器重排序格則將直接禁止某些特定類型的編譯器重排序。
    JMM對于處理器重排序,JMM的處理器重排序規則會要求Java編譯器在生成指令序列時,插入特定類型的內存屏障指令,通過內存屏障來禁止特定類型的處理器重排序。

    內存屏障類型表:

    屏障類型指令示例說明
    LoadLoad BarriersLoad1;loadload;Load2確保Load1數據的裝載完成后,才能執行Load2以及所有后續裝載指令的裝載
    StoreStore BarriersStore1;storestore;Store2確保Store1數據對其他處理器可見(刷新到內存)完成后,才能執行Store2及所有后續存儲指令的存儲
    LoadStore BarriersLoad1;loadstore;Store2確保Load1數據裝載完成后,才能執行Store2及所有后續的存儲指令刷新到內存
    StoreLoad BarriersStore1;storeload;Load2確保Store1數據對其他處理器可見(刷新到內存)完成后,才能執行Load2及所有后續裝載指令的裝載。StoreLoad Barriers會使該屏障之前的所有內存訪問指令完成之后,才執行屏障之后的內存訪問指令

    關于Load屏障:屏障之前的操作執行先于屏障之后的操作,而且后續的操作必須先等當前處理器裝載操作完成,不能我讀的時候讓其他處理器又給更改了,保證當前處理器的讀對其他處理器是可見的。

    關于Store屏障:屏障之前的操作執行先于屏障之后的操作,而且后續的操作必須等我把當前處理器的本地緩存數據刷新到主內存中,不能我正在刷新的時候其他處理器去讀舊數據,保證當前處理器的寫對其他處理器是可見的。

    以上四種屏障StoreLoad Barriers是全能的,但是它的開銷非常大,它要求當前處理器把屏障前涉及的所有緩存區數據全部刷新到內存中。

    所以真正意義上,不是內存屏障真的禁止了處理器重排序,而是處理器重排序無法越過屏障,屏障前的操作始終先于屏障后的操作。

    2.并發編程中的具體體現

    在實際的并發編程中,我們使用volatile關鍵字和synchronized關鍵字來實現有序性保證。

    四.可見性

    JMM通過volatile,final和鎖的內存語義保證可見性。

    1.volatile內存語義

    volatile自身具有以下特性:

    • 可見性。對一個volatile變量的讀,總是能看到任意線程對這個volatile變量最后的寫入
    • 原子性。對任意單個volatile變量的讀/寫本身具有原子性,但是類似于volatile++這種復合操作不具有原子性

    volatile寫的內存語義:
    當寫一個volatile變量時,JMM會把該線程對應的本地內存中的共享變量刷新到主內存中
    volatile讀的內存語義:
    當讀一個volatile變量時,JMM會把該線程的對應的本地內存置為無效,從主內存中讀取共享變量

    如何實現的?
    JMM對此采取的時保守策略,具體策略如下:

    • 在每個volatile寫操作的前面插入一個StoreStore屏障
    • 在每個volatile寫操作的后面插入一個StoreLoad屏障
    • 在每個volatile讀操作的后面插入一個LoadLoad屏障
    • 在每個volatile讀操作的后面插入一個LoadStore屏障

    2.鎖的內存語義

    眾所周知,鎖可以讓臨界區互斥執行,每次只允許一個線程訪問,保證可見性與原子性

    獲取鎖的內存語義:
    當線程獲取鎖時,JMM會把線程對應的本地內存置為無效。從而使得被監視器保護的臨界代碼必須從主內存中讀取內存變量。
    釋放鎖的內存語義:
    當線程釋放鎖時,JMM會把線程對應的本地內存中的共享變量刷新到主內存中。

    如何實現?
    利用synchronized關鍵字或者其他互斥鎖
    利用CAS的非阻塞輕量鎖

    五.JMM的happens-before原則

    happens-before原則是JMM對程序員的保證,讓程序員們安心寫代碼,不用擔心因為重排序或其他非主管因素導致自己程序異常(所以你的并發程序出了問題,別想甩鍋,不關人家編譯器和處理器的事)

    JMM與happens-before原則之間的關系如圖所示:

    《JSR-133》對happens-before原則的定義:

  • 如果一個操作happens-before另一個操作,那么第一個操作的執行結果將對第二個操作可見,而且第一個操作的執行順序排在第二個操作之前。
  • 兩個操作之間存在happens-before關系,并不意味著Java平臺的具體實現一定要按照happens-before原則指定的順序來執行。如果重排序之后的執行結果與按照happens-before關系來執行的結果一致,那么這種重排序并不非法。
  • happens-befores規則:

    • 程序次序規則:一個線程內,按照代碼順序,書寫在前面的操作先行發生于書寫在后面的操作;
    • 鎖定規則:一個unLock()操作先行發生于后面對同一個鎖的lock()操作;
    • volatile變量規則:對一個變量的寫操作先行發生于后面對這個變量的讀操作;
    • 傳遞規則:如果操作A先行發生于操作B,而操作B又先行發生于操作C,則可以得出操作A先行發生于操作C;
    • start()規則:Thread對象的start()方法先行發生于此線程的每個一個動作;
    • interrupt()規則:對線程interrupt()方法的調用先行發生于被中斷線程的代碼檢測到中斷事件的發生;
    • join()規則:線程中所有的操作都先行發生于線程的終止檢測,我們可以通過Thread.join()方法結束、Thread.isAlive()的返回值手段檢測到線程已經終止執行;

    總結

    以上是生活随笔為你收集整理的JMM内存模型如何为并发保驾护航的全部內容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。