日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當(dāng)前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

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

發(fā)布時(shí)間:2025/6/17 编程问答 22 豆豆
生活随笔 收集整理的這篇文章主要介紹了 JMM内存模型如何为并发保驾护航 小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,幫大家做個(gè)參考.

一.為何引入JMM

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

但是引入緩存給計(jì)算機(jī)帶來了新的問題:緩存一致性問題。在目前已經(jīng)很普遍的多處理器計(jì)算機(jī)中,每個(gè)處理器都有著自己的高速緩存,而他們又共享一個(gè)主內(nèi)存,當(dāng)多個(gè)處理器的運(yùn)算任務(wù)都涉及到同一塊內(nèi)存時(shí),就會(huì)導(dǎo)致各自的緩存數(shù)據(jù)不一致。

為了解決緩存一致性問題,需要各個(gè)處理器訪問緩存時(shí)遵守一些協(xié)議,比如MESI協(xié)議等。

以上是引入Java內(nèi)存模型的預(yù)置條件

Java虛擬機(jī)規(guī)范試圖定義一種Java內(nèi)存模型JMM,來屏蔽各種硬件和操作系統(tǒng)的內(nèi)存訪問差異,以實(shí)現(xiàn)讓Java程序在各種平臺下都能達(dá)到一致的內(nèi)存訪問效果,真正做到:“Code once,run everywhere”。這個(gè)模型必須足夠嚴(yán)謹(jǐn),讓Java并發(fā)內(nèi)存訪問操作不會(huì)有歧義,也必須做到足夠?qū)捤?#xff0c;使得虛擬機(jī)的實(shí)現(xiàn)有足夠多自由空間去利用硬件的各種特性來獲取更好的執(zhí)行速度。

Java內(nèi)存模型對并發(fā)的保證主要在于實(shí)現(xiàn)原子性可見性,和有序性三大特性。

二.JMM內(nèi)存模型

并發(fā)編程中,有兩個(gè)關(guān)鍵問題

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

線程之間的通信機(jī)制有兩種:共享內(nèi)存和消息傳遞

  • 共享內(nèi)存
    在共享內(nèi)存的并發(fā)模型中,線程之間共享程序的公共部分,程序必須顯式規(guī)定某些指令必須在線程之間互斥執(zhí)行,這時(shí)同步是顯式實(shí)現(xiàn)的,而消息傳遞是通過共享的內(nèi)存隱式實(shí)現(xiàn)的。

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

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

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

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

    JMM抽象結(jié)構(gòu)示意圖:

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

    CPU 與 Cache 結(jié)構(gòu)圖:



    假設(shè)線程A、B之間要進(jìn)行通信,需要經(jīng)歷一下步驟

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

    三. 原子性

    1.JMM對原子性的保證

    JMM中定義了以下8種原子性操作,虛擬機(jī)實(shí)現(xiàn)時(shí)必須保證其中的每一種都是原子的,不可再分的,這八種原子操作建立在MESI協(xié)議基礎(chǔ)上。

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

    Java內(nèi)存模型還規(guī)定了執(zhí)行上述8種基本操作時(shí)必須滿足如下規(guī)則:

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

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

    2.并發(fā)編程中的具體體現(xiàn)

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

    三.有序性

    1.JMM對有序性的實(shí)現(xiàn)

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

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

  • 編譯器優(yōu)化的重排序

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

  • 指令級并行的重排序

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

  • 內(nèi)存系統(tǒng)的重排序

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

  • 一個(gè)程序可能經(jīng)過的重排序過程

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

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

    內(nèi)存屏障類型表:

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

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

    關(guān)于Store屏障:屏障之前的操作執(zhí)行先于屏障之后的操作,而且后續(xù)的操作必須等我把當(dāng)前處理器的本地緩存數(shù)據(jù)刷新到主內(nèi)存中,不能我正在刷新的時(shí)候其他處理器去讀舊數(shù)據(jù),保證當(dāng)前處理器的寫對其他處理器是可見的。

    以上四種屏障StoreLoad Barriers是全能的,但是它的開銷非常大,它要求當(dāng)前處理器把屏障前涉及的所有緩存區(qū)數(shù)據(jù)全部刷新到內(nèi)存中。

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

    2.并發(fā)編程中的具體體現(xiàn)

    在實(shí)際的并發(fā)編程中,我們使用volatile關(guān)鍵字和synchronized關(guān)鍵字來實(shí)現(xiàn)有序性保證。

    四.可見性

    JMM通過volatile,final和鎖的內(nèi)存語義保證可見性。

    1.volatile內(nèi)存語義

    volatile自身具有以下特性:

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

    volatile寫的內(nèi)存語義:
    當(dāng)寫一個(gè)volatile變量時(shí),JMM會(huì)把該線程對應(yīng)的本地內(nèi)存中的共享變量刷新到主內(nèi)存中
    volatile讀的內(nèi)存語義:
    當(dāng)讀一個(gè)volatile變量時(shí),JMM會(huì)把該線程的對應(yīng)的本地內(nèi)存置為無效,從主內(nèi)存中讀取共享變量

    如何實(shí)現(xiàn)的?
    JMM對此采取的時(shí)保守策略,具體策略如下:

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

    2.鎖的內(nèi)存語義

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

    獲取鎖的內(nèi)存語義:
    當(dāng)線程獲取鎖時(shí),JMM會(huì)把線程對應(yīng)的本地內(nèi)存置為無效。從而使得被監(jiān)視器保護(hù)的臨界代碼必須從主內(nèi)存中讀取內(nèi)存變量。
    釋放鎖的內(nèi)存語義:
    當(dāng)線程釋放鎖時(shí),JMM會(huì)把線程對應(yīng)的本地內(nèi)存中的共享變量刷新到主內(nèi)存中。

    如何實(shí)現(xiàn)?
    利用synchronized關(guān)鍵字或者其他互斥鎖
    利用CAS的非阻塞輕量鎖

    五.JMM的happens-before原則

    happens-before原則是JMM對程序員的保證,讓程序員們安心寫代碼,不用擔(dān)心因?yàn)橹嘏判蚧蚱渌侵鞴芤蛩貙?dǎo)致自己程序異常(所以你的并發(fā)程序出了問題,別想甩鍋,不關(guān)人家編譯器和處理器的事)

    JMM與happens-before原則之間的關(guān)系如圖所示:

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

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

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

    總結(jié)

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

    如果覺得生活随笔網(wǎng)站內(nèi)容還不錯(cuò),歡迎將生活随笔推薦給好友。