目錄
一. 前言
二. 為什么會有內(nèi)存模型
2.1.?硬件內(nèi)存架構
2.2.?緩存一致性問題
2.3.?處理器優(yōu)化和指令重排序
三.?并發(fā)編程的問題
四. Java 內(nèi)存模型(JMM)
4.1.?Java 運行時內(nèi)存區(qū)域與硬件內(nèi)存的關系
4.2.?Java 線程與主內(nèi)存的關系
4.3.?線程間通信
五. 主內(nèi)存和工作內(nèi)存
六.?Java 內(nèi)存模型的實現(xiàn)
6.1. 原子性
6.2. 可見性
6.3. 有序性
七. 總結
一. 前言
? ? 網(wǎng)上有很多關于 Java 內(nèi)存模型的文章,在《深入理解Java虛擬機》和《Java并發(fā)編程的藝術》等書中也都有關于這個知識點的介紹。但是,很多人讀完之后還是搞不清楚,甚至有的人說自己更懵了。本文,就來整體的介紹一下 Java 內(nèi)存模型,目的很簡單,讓你讀完本文以后,就知道到底Java 內(nèi)存模型是什么,為什么要有 Java 內(nèi)存模型,Java 內(nèi)存模型解決了什么問題等。
二. 為什么會有內(nèi)存模型
? ? 要想回答這個問題,我們需要先弄懂傳統(tǒng)計算機硬件內(nèi)存架構。光用文字可能說不清楚,下面通過幾張圖來幫助理解。
2.1.?硬件內(nèi)存架構

1. CPU
? ? 去過機房的同學都知道,一般在大型服務器上會配置多個 CPU,每個 CPU 還會有多個核,這就意味著多個 CPU 或者多個核可以同時(并發(fā))工作。如果使用 Java 起了一個多線程的任務,很有可能每個 CPU 都會跑一個線程,那么你的任務在某一刻就是真正并發(fā)執(zhí)行了。
2. CPU 寄存器(CPU Register)
? ? CPU Register 也就是 CPU 寄存器。CPU 寄存器是 CPU 內(nèi)部集成的,在寄存器上執(zhí)行操作的效率要比在主存上高出幾個數(shù)量級。
3.?CPU 高速緩存(CPU Cache Memory)
? ? CPU Cache Memory 也就是 CPU 高速緩存,相對于寄存器來說,通常也可以成為 L2 二級緩存。相對于硬盤讀取速度來說內(nèi)存讀取的效率非常高,但是與 CPU 還是相差數(shù)量級,所以在 CPU 和主存間引入了多級緩存,目的是為了做一下緩沖。
4.?主存(Main Memory)
Main Memory 就是主存,主存比 L1、L2 緩存要大很多。
注意:部分高端機器還有 L3 三級緩存。
2.2.?緩存一致性問題
? ? 由于主存與 CPU 處理器的運算能力之間有數(shù)量級的差距,所以在傳統(tǒng)計算機內(nèi)存架構中會引入高速緩存來作為主存和處理器之間的緩沖,CPU 將常用的數(shù)據(jù)放在高速緩存中,運算結束后 CPU 再將運算結果同步到主存中。
使用高速緩存解決了 CPU 和主存速率不匹配的問題,但同時又引入另外一個新問題:緩存一致性問題。

? ? 在多 CPU 的系統(tǒng)中(或者單 CPU 多核的系統(tǒng)),每個 CPU 內(nèi)核都有自己的高速緩存,它們共享同一主內(nèi)存(Main Memory)。當多個 CPU 的運算任務都涉及同一塊主內(nèi)存區(qū)域時,CPU 會將數(shù)據(jù)讀取到緩存中進行運算,這可能會導致各自的緩存數(shù)據(jù)不一致。
? ? 因此需要每個 CPU 訪問緩存時遵循一定的協(xié)議,在讀寫數(shù)據(jù)時根據(jù)協(xié)議進行操作,共同來維護緩存的一致性。這類協(xié)議有 MSI、MESI、MOSI 和 Dragon Protocol 等。
2.3.?處理器優(yōu)化和指令重排序
? ? 為了提升性能在 CPU 和主內(nèi)存之間增加了高速緩存,但在多線程并發(fā)場景可能會遇到緩存一致性問題。那還有沒有辦法進一步提升 CPU 的執(zhí)行效率呢?答案是:處理器優(yōu)化。
? ? 為了使處理器內(nèi)部的運算單元能夠最大化被充分利用,處理器會對輸入代碼進行亂序執(zhí)行處理,這就是處理器優(yōu)化。
? ? 除了處理器會對代碼進行優(yōu)化處理,很多現(xiàn)代編程語言的編譯器也會做類似的優(yōu)化,比如像 Java 的即時編譯器(JIT)會做指令重排序。
處理器優(yōu)化其實也是重排序的一種類型,這里總結一下,重排序可以分為三種類型:
- 編譯器優(yōu)化的重排序。編譯器在不改變單線程程序語義放入前提下,可以重新安排語句的執(zhí)行順序。
- 指令級并行的重排序?,F(xiàn)代處理器采用了指令級并行技術來將多條指令重疊執(zhí)行。如果不存在數(shù)據(jù)依賴性,處理器可以改變語句對應機器指令的執(zhí)行順序。
- 內(nèi)存系統(tǒng)的重排序。由于處理器使用緩存和讀寫緩沖區(qū),這使得加載和存儲操作看上去可能是在亂序執(zhí)行。?
三.?并發(fā)編程的問題
? ? 上面講了一堆硬件相關的東西,有些同學可能會有點懵,繞了這么大圈,這些東西跟 Java 內(nèi)存模型有啥關系嗎?不要急咱們慢慢往下看。
? ? 熟悉 Java 并發(fā)的同學肯定對這三個問題很熟悉:可見性問題、原子性問題、有序性問題。如果從更深層次看這三個問題,其實就是上面講的緩存一致性、處理器優(yōu)化、指令重排序造成的。
? ? 緩存一致性問題其實就是可見性問題,處理器優(yōu)化可能會造成原子性問題,指令重排序會造成有序性問題,你看是不是都聯(lián)系上了。
? ? 出了問題總是要解決的,那有什么辦法呢?首先想到簡單粗暴的辦法,干掉緩存讓 CPU 直接與主內(nèi)存交互就解決了可見性問題,禁止處理器優(yōu)化和指令重排序就解決了原子性和有序性問題,但這樣一夜回到解放前了,顯然不可取。
? ? 所以技術前輩們想到了在物理機器上定義出一套內(nèi)存模型,規(guī)范內(nèi)存的讀寫操作。內(nèi)存模型解決并發(fā)問題主要采用兩種方式:限制處理器優(yōu)化和使用內(nèi)存屏障。
四. Java 內(nèi)存模型(JMM)
? ? 同一套內(nèi)存模型規(guī)范,不同語言在實現(xiàn)上可能會有些差別。接下來著重講一下 Java 內(nèi)存模型實現(xiàn)原理。
4.1.?Java 運行時內(nèi)存區(qū)域與硬件內(nèi)存的關系
? ? 了解過 JVM 的同學都知道,JVM 運行時內(nèi)存區(qū)域是分片的,分為棧、堆等,其實這些都是 JVM 定義的邏輯概念。在傳統(tǒng)的硬件內(nèi)存架構中是沒有棧和堆這種概念。
從圖中可以看出棧和堆既存在于高速緩存中又存在于主內(nèi)存中,所以兩者并沒有很直接的關系。
4.2.?Java 線程與主內(nèi)存的關系
Java 內(nèi)存模型是一種規(guī)范,定義了很多東西:
- 所有的變量都存儲在主內(nèi)存(Main Memory)中。
- 每個線程都有一個私有的本地內(nèi)存(Local Memory),本地內(nèi)存中存儲了該線程以讀/寫共享變量的拷貝副本。
- 線程對變量的所有操作都必須在本地內(nèi)存中進行,而不能直接讀寫主內(nèi)存。
- 不同的線程之間無法直接訪問對方本地內(nèi)存中的變量。
看文字比較枯燥,看下面這張圖:

4.3.?線程間通信
? ? 如果兩個線程都對一個共享變量進行操作,共享變量初始值為 1,每個線程都變量進行加 1,預期共享變量的值為 3。在 JMM 規(guī)范下會有一系列的操作。
為了更好的控制主內(nèi)存和本地內(nèi)存的交互,Java 內(nèi)存模型定義了八種操作來實現(xiàn):
- 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)存的變量中。?
注意:工作內(nèi)存也就是本地內(nèi)存的意思。
五. 主內(nèi)存和工作內(nèi)存
? ? 由于 CPU 和主內(nèi)存間存在數(shù)量級的速率差,想到了引入多級高速緩存的傳統(tǒng)硬件內(nèi)存架構來解決,多級高速緩存作為 CPU 和主內(nèi)存間的緩沖提升了整體性能。解決了速率差的問題,卻又帶來了緩存一致性問題。
? ? 數(shù)據(jù)同時存在于高速緩存和主內(nèi)存中,如果不加以規(guī)范勢必造成災難,因此在傳統(tǒng)機器上又抽象出了內(nèi)存模型。
? ? Java 語言在遵循內(nèi)存模型的基礎上推出了 JMM 規(guī)范,目的是解決由于多線程通過共享內(nèi)存進行通信時,存在的本地內(nèi)存數(shù)據(jù)不一致、編譯器會對代碼指令重排序、處理器會對代碼亂序執(zhí)行等帶來的問題。
? ? 為了更精準控制工作內(nèi)存和主內(nèi)存間的交互,JMM 還定義了八種操作:lock、unlock、read、 load、use、assign、store、write。
? ? 前面介紹過了計算機內(nèi)存模型,這是解決多線程場景下并發(fā)問題的一個重要規(guī)范。那么具體的實現(xiàn)是如何的呢,不同的編程語言,在實現(xiàn)上可能有所不同。
? ? 我們知道,Java 程序是需要運行在 Java 虛擬機上面的,Java 內(nèi)存模型(Java Memory Model,JMM)就是一種符合內(nèi)存模型規(guī)范的,屏蔽了各種硬件和操作系統(tǒng)的訪問差異的,保證了Java 程序在各種平臺下對內(nèi)存的訪問都能保證效果一致的機制及規(guī)范。
? ? 提到 Java 內(nèi)存模型,一般指的是 JDK 5 開始使用的新的內(nèi)存模型,主要由 JSR-133: JavaTM Memory Model and Thread Specification 描述。感興趣的可以參看下這份 PDF 文檔:http://www.cs.umd.edu/~pugh/java/memoryModel/jsr133.pdf
? ? Java 內(nèi)存模型規(guī)定了所有的變量都存儲在主內(nèi)存中,每條線程還有自己的工作內(nèi)存,線程的工作內(nèi)存中保存了該線程中是用到的變量的主內(nèi)存副本拷貝,線程對變量的所有操作都必須在工作內(nèi)存中進行,而不能直接讀寫主內(nèi)存。不同的線程之間也無法直接訪問對方工作內(nèi)存中的變量,線程間變量的傳遞均需要在自己的工作內(nèi)存和主存之間進行數(shù)據(jù)同步進行。
? ? 而 JMM 就是作用于工作內(nèi)存和主存之間的數(shù)據(jù)同步過程。他規(guī)定了如何做數(shù)據(jù)同步以及什么時候做數(shù)據(jù)同步。
? ? 這里面提到的主內(nèi)存和工作內(nèi)存,讀者可以簡單的類比成計算機內(nèi)存模型中的主存和緩存的概念。特別需要注意的是,主內(nèi)存和工作內(nèi)存與 JVM 內(nèi)存結構中的 Java 堆、棧、方法區(qū)等并不是同一個層次的內(nèi)存劃分,無法直接類比?!渡钊肜斫釰ava虛擬機》中認為,如果一定要勉強對應起來的話,從變量、主內(nèi)存、工作內(nèi)存的定義來看,主內(nèi)存主要對應于 Java 堆中的對象實例數(shù)據(jù)部分。工作內(nèi)存則對應于虛擬機棧中的部分區(qū)域。
? ? 所以,JMM 是一種規(guī)范,目的是解決由于多線程通過共享內(nèi)存進行通信時,存在的本地內(nèi)存數(shù)據(jù)不一致、編譯器會對代碼指令重排序、處理器會對代碼亂序執(zhí)行等帶來的問題。
六.?Java 內(nèi)存模型的實現(xiàn)
? ? 了解 Java 多線程的朋友都知道,在 Java 中提供了一系列和并發(fā)處理相關的關鍵字,比如volatile、synchronized、final、concurrent 包等。其實這些就是 Java 內(nèi)存模型封裝了底層的實現(xiàn)后提供給程序員使用的一些關鍵字。
? ? 在開發(fā)多線程的代碼的時候,我們可以直接使用 synchronized 等關鍵字來控制并發(fā),從來就不需要關心底層的編譯器優(yōu)化、緩存一致性等問題。所以,Java 內(nèi)存模型,除了定義了一套規(guī)范,還提供了一系列原語,封裝了底層實現(xiàn)后,供開發(fā)者直接使用。
? ? 本文并不準備把所有的關鍵字逐一介紹其用法,關于各個關鍵字的用法,可以參見《Java 之 volatile 詳解》、《深入理解 synchronized 原理》、《Java 之 final 詳解》、《Java 中的全部鎖》、《J.U.C家族》、《JUC之Lock及核心AQS》、《JUC之Atomic原子類》等文章???????。本文還有一個重點要介紹的就是,我們前面提到,并發(fā)編程要解決原子性、有序性和一致性的問題,我們就再來看下,在 Java 中,分別使用什么方式來保證。
6.1. 原子性
? ? 在 Java 中,為了保證原子性,提供了兩個高級的字節(jié)碼指令 monitorenter 和 monitorexit。在《深入理解 synchronized 原理》一文中,介紹過這兩個字節(jié)碼,在 Java 中對應的關鍵字就是synchronized。
? ? 因此,在 Java 中可以使用 synchronized 來保證方法和代碼塊內(nèi)的操作是原子性的。
6.2. 可見性
? ? Java 內(nèi)存模型是通過在變量修改后將新值同步回主內(nèi)存,在變量讀取前從主內(nèi)存刷新變量值的這種依賴主內(nèi)存作為傳遞媒介的方式來實現(xiàn)的。
? ? Java 中的 volatile 關鍵字提供了一個功能,那就是被其修飾的變量在被修改后可以立即同步到主內(nèi)存,被其修飾的變量在每次用之前都從主內(nèi)存刷新。因此,可以使用 volatile 來保證多線程操作時變量的可見性。
? ? 除了 volatile,Java 中的 synchronized 和 final 兩個關鍵字也可以實現(xiàn)可見性。只不過實現(xiàn)方式不同,這里不再展開了。
6.3. 有序性
? ? 在 Java 中,可以使用 synchronized 和 volatile 來保證多線程之間操作的有序性。實現(xiàn)方式有所區(qū)別:volatile 關鍵字會禁止指令重排。synchronized 關鍵字保證同一時刻只允許一條線程操作。
至此,就已經(jīng)簡單地介紹完了 Java 并發(fā)編程中解決原子性、可見性以及有序性可以使用的關鍵字。讀者可能發(fā)現(xiàn)了,好像 synchronized 關鍵字是萬能的,他可以同時滿足以上三種特性,這其實也是很多人濫用 synchronized 的原因。
但是 synchronized 是比較影響性能的,雖然編譯器提供了很多鎖優(yōu)化技術,但是也不建議過度使用。文章來源:http://www.zghlxwxcb.cn/news/detail-855366.html
七. 總結
? ? 讀到此處的同學們,相信你應該已經(jīng)了解了什么是 Java 內(nèi)存模型、Java 內(nèi)存模型的作用以及Java 中內(nèi)存模型做了什么事情等。關于 Java 內(nèi)存模型的有關知識,如果還想繼續(xù)深入,可以參考《深入理解Java虛擬機》和《Java并發(fā)編程的藝術》兩本書。文章來源地址http://www.zghlxwxcb.cn/news/detail-855366.html
到了這里,關于Java 內(nèi)存模型(JMM)探尋原理,深度講解的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關文章,希望大家以后多多支持TOY模板網(wǎng)!