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

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程语言 > java >内容正文

java

Java 内存模型(JMM)

發布時間:2023/12/10 java 28 豆豆
生活随笔 收集整理的這篇文章主要介紹了 Java 内存模型(JMM) 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

文章目錄

    • 什么是JMM模型
    • JMM和JVM的區別
    • 主內存和工作內存
    • Java內存模型與硬件內存架構的關系
    • JMM存在的必要性
    • JMM數據同步八大原子操作
    • CPU緩存一致性協議MESI
      • 為什么要有高速緩存?
      • 帶有高速緩存的CPU執行計算的流程
      • 目前流行的多級緩存結構
    • CPU底層全執行流程
    • 同步規則分析
    • 并發編程的可見性、原子性和有序性問題
    • JMM是如何解決原子性&可見性&有序性問題
    • 指令重排序
    • as-if-serial 語義
    • happens-before原則

什么是JMM模型

Java內存模型(Java Memory Model簡稱JMM)是一種抽象的概念,并不真實存在,它描述的是一組規則或規范,通過這組規范定義了程序中各個變量(包括實例字段,靜態字段和構成數組對象的元素)的訪問方式。

JVM運行程序的實體是線程,而每個線程創建時,JVM都會為其創建一個工作內存(有些地方稱為棧空間),用于存儲線程私有的數據。

Java內存模型中規定所有變量都存儲在主內存,主內存是共享內存區域,所有線程都可以訪問,但線程對變量的操作(讀取賦值等)必須在工作內存中進行,首先要將變量從主內存拷貝的自己的工作內存空間,然后對變量進行操作,操作完成后再將變量寫回主內存,不能直接操作主內存中的變量,工作內存中存儲著主內存中的變量副本拷貝。

JMM和JVM的區別

  • JMM是圍繞原子性,有序性、可見性展開。
  • JMM與Java內存區域唯一相似點,都存在共享數據區域和私有數據區域,在JMM中主內存屬于共享數據區域,從某個程度上講應該包括了堆和方法區,而工作內存數據線程私有數據區域,從某個程度上講則應該包括程序計數器、虛擬機棧以及本地方法棧。

主內存和工作內存

  • 主內存
  • 主要存儲的是Java實例對象,所有線程創建的 實例對象都存放在主存中,不管該實例對象時成員邊浪還是方法中的本地變量,當然還包含共享的類信息、常量、靜態 變量。由于是共享數據區域,多條線程對同一個變量進行訪問可能會發生線程安全問題。

  • 工作內存
    • 主要存儲當前方法的所有本地變量(工作內存中存儲著主內存中的變量副本拷貝),每個線程只能訪問自己的工作內存,即線程中的本地變量對其他線程是不可見的,就算是兩個線程執行的是同一段代碼,它們也會各自在自己的工作內存中創建屬于當前線程的本地變量,當然也包括了字節碼行號指示器、相關Native方法的信息。
    • 由于工作內存是每個線程的私有數據,線程間無法相互訪問工作內存,因此存儲在工作內存的數據不存在線程安全問題。
    • 根據JVM 虛擬機規范主內存與工作內存的數據存儲類型以及操作方式,對于一個實例對象中的成員方法而言,如果方法中包含本地變量是基本類型,將直接存儲在工作內存的棧幀結構中,但倘若本地變量是引用類型,那么該變量的引用會存儲在工作內存的棧幀中,而對象實例將存儲在主內存(共享內存區域,堆中)。
    • 但對于實例對象的成員變量,不管它是基本數據類型或者包裝類型還是引用類型,都會存儲在堆中。
    • 至于static變量以及類本身相關信息將會存儲在主內存中。
    • 在主內存中的實例對象可以被多線程共享,倘若兩個線程同時調用了同一對象的同一方法,那么兩條線程會將要操作的數據拷貝一份到自己的工作內存中,執行完成操作后才刷新到主內存中。

    Java內存模型與硬件內存架構的關系

    對于硬件內存來說只有寄存器、緩存內存、主內存的概念,并沒有工作內存之分,也就是說Java內存模型對內存的劃分對硬件內存并沒有任何影響,因為JMM只是一種抽象的概念,是一組規則,并不實際存在,不管是工作內存的數據還是主 內存的數據,對于計算機硬件來說都會存儲在計算機主內存中,當然也有可能存儲在到CPU緩存或者寄存器中

    因此總體上來 說,Java內存模型和計算機硬件內存架構是一個相互交叉的關系,是一種抽象概念劃分與真實物理硬件的交互。

    JMM存在的必要性

    由于JVM運行程序的實體是線程,而每個線程創建時JVM都會為其創建一個工作內存(有些地方稱為棧空間),用于存儲線程私有的數據,線程與主內存中的變量操作必須通過工作內存間接完成,主要過程是將變量從主內存拷貝的每個線程各自的工作內存空間,然后對變量進行操作,操作完成后再將變量寫回主內存,如果存在兩個線程同時對一個主內存中的實例對象的變量進行操作就有可能誘發線程安全問題。


    假設主內存中存在一個共享變量X,現在有A和B兩條線程分別對該變量x=1進行操作,A和B線程各自在自己的工作內存中存在共享變量副本X.

    假設現在A線程想要修改x的值 為2,而B線程卻想要讀取X的值,那么B線程讀取到的值是A線程更新后的值2還是更新前的值1呢?答案是不確定,即B線程 有可能讀取到A線程更新前的值1,也有可能讀取到A 線程更新后的值2,這是因為工作內存是每個線程私有的數據區域,而線程A變量x時,首先是講變量從主內存拷貝到A線程的工作內存中,然后對變量進行操作,操作完成 后再將變量X寫回主內存中,而對于B線程也是類似,這樣就有可能造成主內存與工作內存間數據存在不一致性問題。

    假設A線程修改完成后正在將數據寫回主內存,而B線程此時正在讀取主內存,即將x=1拷貝到自己的工作呢次云中,這樣B線程讀取的值就是x=1,但如果A線程已將x=2寫回主內存后,B線程才開始讀取的話,那么此時B線程讀取的就是x=2,但到底是哪種情況先發生呢?

    JMM數據同步八大原子操作


    (1)lock(鎖定):作用于主內存的變量,把一個變量標記為一條線程獨占狀態

    (2)unlock(解鎖):作用于主內存的變量,把一個處于鎖定狀態的變量釋放出來,釋放后的變量才可以被其他線程鎖定。
    (3)read(讀取):作用于主內存的變量,把一個變量值從主內存傳輸到線程的工作內存中,以便隨后的load動作使用。
    (4)load(載入):作用于工作內存的變量,它把read操作從主內存中得到的變量值放入工作內存的變量副本中。
    (5)use(使用):作用于工作內存的變量,把工作內存中的一個變量值傳遞給執行引擎。
    (6)assign(賦值):作用于工作內存的變量,它把一個從執行引擎接收到的值賦給工作內存的變量。
    (7)store(存儲):作用于工作內存的變量,把工作內存中的一個變量的值傳送到主內存中,以便隨后的write的操作。
    (8)write(寫入):作用于工作內存的變量,它把store操作從工作內存中的一個變量的值傳送到主內存的變量中。

    CPU緩存一致性協議MESI

    為什么要有高速緩存?

    • CPU在摩爾定律的指導下以每18個月翻一番的速度在發展,然而內存和硬盤的發展速度遠遠不及CPU。這就造成了高性能能的內存和硬盤價格及其昂貴。然而CPU的高度運算需要高速的數據。為了解決這個問題,CPU廠商在CPU中內置了少量的高速緩存以解決I\O速度和CPU運算速度之間的不匹配問題。在CPU訪問存儲設備時,無論是存取數據抑或存取指令,都趨于聚集在一片連續的區域中,這就被稱為局部性原理。

    • 時間局部 性

      如果一個信息項正在被訪問,那么在近期它很可能還會被再次訪問。比如循環、遞歸、方法的反復調用等。

    • 空間局部性

      如果一個存儲器的位置被引用,那么將來他附近的位置也會被引用。比如順序執行的代碼、連續創建的兩個對象、數組等。

    帶有高速緩存的CPU執行計算的流程


    1.程序以及數據被加載到主內存
    2.指令和數據被加載到CPU的高速緩存
    3.CPU執行指令,把結果寫到高速緩存
    4.高速緩存中的數據寫回主內存

    目前流行的多級緩存結構

    由于CPU的運算速度超越了1級緩存的數據I\O能力,CPU廠商又引入了多級的緩存結構。

    CPU底層全執行流程

    同步規則分析

    1)不允許一個線程無原因地(沒有發生過任何assign操作)把數據從工作內存同步回主內存中。
    2)一個新的變量只能在主內存中誕生,不允許在工作內存中直接使用一個未被初始化(load或者assign)的變量。即就是對一個變量實施use和store操作之前,必須先自行assign和load操作。
    3)一個變量在同一時刻只允許一條線程對其進行lock操作,但lock操作可以被同一線程重復執行多次,多次執行lock后,只有執行相同次數的unlock操作,變量才會被解鎖。lock和unlock必須成對出現。
    4)如果對一個變量執行lock操作,將會清空工作內存中此變量的值,在執行引擎使用這個變量之前需要重新執行load或assign操作初始化變量的值。
    5)如果一個變量事先沒有被lock操作鎖定,則不允許對它執行unlock操作;也不允許去unlock一個被其他線程鎖定的變量。
    6)對一個變量執行unlock操作之前,必須先把此變量同步到主內存中(執行store和write操作。

    并發編程的可見性、原子性和有序性問題

  • 原子性

    原子性指的是一個操作是不可中斷的,即使是在多線程環境下,一個操作一旦開始就不會被其他線程影響。在java中,對基本數據類型的變量的讀取和賦值操作是原子性操作有點要注意的是,對于32位系統的來說,long類型數據和double類型數據(對于基本數據類型,byte,short,int,float,boolean,char讀寫是原子操作),它們的讀寫并非原子性的,也就是說如果存在兩條線程同時對long類型或者double類型的數據進行讀寫是存在相互干擾的,因為對于32位虛擬機來說,每次原子讀寫是32位的,而long和double則是64位的存儲單元,這樣會導致一個線程在寫時,操作完前32位的原子操作后,輪到B線程讀取時,恰好只讀取到了后32位的數據,這樣可能會讀取到一個既非原值又不是線程修改值的變量,它可能是“半個變量”的數值,即64位數據被兩個線程分成了兩次讀取。但也不必太擔心,因為讀取到“半個變量”的情況比較少見,至少在目前的商用的虛擬機中,幾乎都把64位的數據的讀寫操作作為原子操作來執行,因此對于這個問題不必太在意,知道這么回事即可

  • 可見性

    • 可見性指的是當一個線程修改了某個共享變量的值,其他線程是否能夠馬上得知這個修改的值。對于串行程序來說,可見性是不存在的,因為我們在任何一個操作中修改了某個變量的值,后續的操作中都能讀取這個變量值,并且是修改過的新值。
    • 但在多線程環境中可就不一定了,前面我們分析過,由于線程對共享變量的操作都是線程拷貝到各自的工作內存進行操作后才寫回到主內存中的,這就可能存在一個線程A修改了共享變量x的值,還未寫回主內存時,另外一個線程B又對主內存中同一個共享變量x進行操作,但此時A線程工作內存中共享變量x對線程B來說并不可見,這種工作內存與主內存同步延遲現象就造成了可見性問題,另外指令重排以及編譯器優化也可能導致可見性問題,通過前面的分析,我們知道無論是編譯器優化還是處理器優化的重排現象,在多線程環境下,確實會導致程序輪序執行的問題,從而也就導致可見性問題。
  • 有序性

    有序性是指對于單線程的執行代碼,我們總是認為代碼的執行是按順序依次執行的,這樣的理解并沒有毛病,畢竟對于單線程而言確實如此,但對于多線程環境,則可能出現亂序現象,因為程序編譯成機器碼指令后可能會出現指令重排現象,重排后的指令與原指令的順未必一致,要明白的是,在Java程序中,倘若在本線程內,所有操作都視為有序行為,如果是多線程環境下,一個線程中觀察另外一個線程,所有操作都是無序的,前半句指的是單線程內保證串行語義執行的一致性,后半句則指指令重排現象和工作內存與主內存同步延遲現象。

  • JMM是如何解決原子性&可見性&有序性問題

  • 原子性問題
    除了JVM自身提供的對基本數據類型讀寫操作的原子性外,可以通過 synchronized和Lock實現原子性。因為synchronized和Lock能夠保證任一時刻只有一個線程訪問該代碼塊。

  • 可見性問題

    • volatile關鍵字保證可見性。當一個共享變量被volatile修飾時,它會保證修改的值立即被其他的線程看到,即修改的值立即更新到主存中,當其他線程需要讀取時,它會去內存中讀取新值。
    • synchronized和Lock也可以保證可見性,因為它們可以保證任一時刻只有一個線程能訪問共享資源,并在其釋放鎖之前將修改的變量刷新到內存中。
  • 有序性問題
    • 通過volatile可以保證有序性。
    • 可以通過synchronized和Lock來保證有序性,很顯然,synchronized和Lock保證每個時刻是有一個線程執行同步代碼,相當于是讓線程順序執行同步代碼,自然就保證了有序性。

    指令重排序

    Java語言規范規定JVM線程內部維持順序化語義。即只要程序的最終結果與它的順序情況的結果相等,那么指令的執行順序可以與代碼順序不一樣,此過程叫指令你的重排序

    指令重排序的意義是什么?

    JVM能根據處理器特征適當的對機器指令進行重排序,使機器指令能更符合CPU的執行特性,最大限度的發揮機器特性。

    as-if-serial 語義

    不管怎么重排序,程序的執行結果不能被改變。編譯器、runtime和處理器都必須遵守as-if-serial語義。

    為了遵守as-if-serial 語義,編譯器和處理器不會對存在數據依賴關系的操作做重排序,因為這種重排序會改變執行結果。但是操作之間不存在數據依賴關系,這些操作就可能被編譯器和處理器重排序。


    happens-before原則

    只靠synchronized和volatile關鍵字來保證原子性、可見性以及有序性,那么編寫并發編程顯得十分麻煩,從JDK5開始,Java使用新的JSR-133內存模型,提供了happeds-before原則來輔助保證程序執行的原子性、可見性以及有序性的問題,它是判斷數據是否存在競爭、線程是否安全的依據。

    程序順序原則,即在一個線程內必須保證語義串行性,也就是說按照代碼順序執行。

    鎖規則:解鎖(unlock)操作必然發生在后續的同一個鎖的加鎖(lock)之前,也就是說,如果對于一個鎖解鎖后,再加鎖,那么加鎖的動作必須在解鎖動作之后(同一個鎖)。

    volatile規則: volatile變量的寫,先發生于讀,這保證了volatile變量的可見性,簡單的理解就是,volatile變量在每次被線程訪問時,都強迫從主內存中讀該變量的值,而當該變量發生變化時,又會強迫將最新的值刷新到主內存,任何時刻,不同的線程總是能夠看到該變量的最新值。

    線程啟動規則: 線程的start()方法先于它的每一個動作,即如果線程A在執行線程B的start方法之前修改了共享變量的值,那么當線程B執行start方法時,線程A對共享變量的修改對線程B可見。

    傳遞性 A先于B ,B先于C 那么A必然先于C

    線程終止規則: 線程的所有操作先于線程的終結,Thread.join()方法的作用是等待當前執行的線程終止。假設在線程B終止之前,修改了共享變量,線程A從線程B的join方法成功返回后,線程B對共享變量的修改將對線程A可見。

    線程中斷規則: 對線程 interrupt()方法的調用先行發生于被中斷線程的代碼檢測到中斷事件的發生,可以通過Thread.interrupted()方法檢測線程是否中斷。

    對象終結規則對象的構造函數執行,結束先于finalize()方法

    總結

    以上是生活随笔為你收集整理的Java 内存模型(JMM)的全部內容,希望文章能夠幫你解決所遇到的問題。

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