自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

從 CPU 說(shuō)起,深入理解 Java 內(nèi)存模型!

開發(fā) 前端
這篇文章我們從底層 CPU 開始講起,一直講到操作系統(tǒng),最后講到了編程語(yǔ)言層面,讓大家能夠一環(huán)扣一環(huán)地理解,最后明白 Java 內(nèi)存模型誕生的原因(上層有數(shù)據(jù)一致性問題),以及最終要解決的問題(緩存一致性問題)。

Java 內(nèi)存模型,許多人會(huì)錯(cuò)誤地理解成 JVM 的內(nèi)存模型。但實(shí)際上,這兩者是完全不同的東西。Java 內(nèi)存模型定義了 Java 語(yǔ)言如何與內(nèi)存進(jìn)行交互,具體地說(shuō)是 Java 語(yǔ)言運(yùn)行時(shí)的變量,如何與我們的硬件內(nèi)存進(jìn)行交互的。而 JVM 內(nèi)存模型,指的是 JVM 內(nèi)存是如何劃分的。

Java 內(nèi)存模型是并發(fā)編程的基礎(chǔ),只有對(duì) Java 內(nèi)存模型理解較為透徹,我們才能避免一些錯(cuò)誤地理解。Java 中一些高級(jí)的特性,也建立在 Java 內(nèi)存模型的基礎(chǔ)上,例如:volatile 關(guān)鍵字。

為了讓大家能明白 Java 內(nèi)存模型存在的意義,本篇文章將從計(jì)算機(jī)硬件出發(fā),一路寫到操作系統(tǒng)、編程語(yǔ)言,一環(huán)扣一環(huán)的引出 Java 內(nèi)存模型存在的意義,讓大家對(duì) Java 內(nèi)存模型有較為深刻的理解??赐曛?,希望大家能夠明白如下幾個(gè)問題:

  • 為什么要有 Java 內(nèi)存模型?
  • Java 內(nèi)存模型解決了什么問題?
  • Java 內(nèi)存模型是怎樣的一個(gè)東西?

從 CPU 說(shuō)起

我們知道計(jì)算機(jī)有 CPU 和內(nèi)存兩個(gè)東西,CPU 負(fù)責(zé)計(jì)算,內(nèi)存負(fù)責(zé)存儲(chǔ)數(shù)據(jù),每次 CPU 計(jì)算前都需要從內(nèi)存獲取數(shù)據(jù)。我們知道 CPU 的運(yùn)行速度遠(yuǎn)遠(yuǎn)快于內(nèi)存的速度,因此會(huì)出現(xiàn) CPU 等待內(nèi)存讀取數(shù)據(jù)的情況。

由于兩者的速度差距實(shí)在太大,我們?yōu)榱思涌爝\(yùn)行速度,于是計(jì)算機(jī)的設(shè)計(jì)者在 CPU 中加了一個(gè) CPU 高速緩存。這個(gè) CPU 高速緩存的速度介于 CPU 與內(nèi)存之間,每次需要讀取數(shù)據(jù)的時(shí)候,先從內(nèi)存讀取到 CPU 緩存中,CPU 再?gòu)?CPU 緩存中讀取。這樣雖然還是存在速度差異,但至少不像之前差距那么大了。

圖片

新增 CPU 高速緩存

隨著技術(shù)的發(fā)展,多核 CPU 出現(xiàn)了,CPU 的計(jì)算能力進(jìn)一步提高。原本同一時(shí)間只能運(yùn)行一個(gè)任務(wù),但現(xiàn)在可以同時(shí)運(yùn)行多個(gè)任務(wù)。由于多核 CPU 的出現(xiàn),雖然提高了 CPU 的處理速度,但也帶來(lái)了新的問題:緩存一致性。

在多 CPU 系統(tǒng)中,每個(gè)處理器都有自己的高速緩存,而它們又共享同一主內(nèi)存,如下圖所示。當(dāng)多個(gè) CPU 的運(yùn)算任務(wù)都涉及同一塊主內(nèi)存區(qū)域時(shí),可能導(dǎo)致各自的緩存數(shù)據(jù)不一致。如果發(fā)生了這種情況,那同步回主內(nèi)存時(shí)以哪個(gè) CPU 高速緩存的數(shù)據(jù)為準(zhǔn)呢?

圖片

多核 CPU 及高速緩存導(dǎo)致的問題

我們舉個(gè)例子,線程 A 執(zhí)行這樣一段代碼:

i = i + 10;

線程 B 執(zhí)行這樣一段代碼:

i = i + 10;

他們的 i 都是存儲(chǔ)在內(nèi)存中共用的,初始值是 0。按照我們的設(shè)想,最終輸出的值應(yīng)該是 20 才對(duì)。但實(shí)際上有可能輸出的值是 10。下面是可能發(fā)生的一種情況:

  • 線程 A 分配到 CPU0 執(zhí)行,這時(shí)候讀取 i 的值為 0,存到 CPU0 的高速緩存中。
  • 線程 B 分配到 CPU1 執(zhí)行,這時(shí)候讀取 i 的值為 0,存到 CPU1 的高速緩存中。
  • CPU0 進(jìn)行運(yùn)算,得出結(jié)果 10,運(yùn)算結(jié)束,寫回內(nèi)存,此時(shí)內(nèi)存 i 的值為 10。
  • CPU1 進(jìn)行運(yùn)算,得出結(jié)果 10,運(yùn)算結(jié)束,寫回內(nèi)存,此時(shí)內(nèi)存 i 的值為 10。

可以看到發(fā)生錯(cuò)誤結(jié)果的主要原因是:兩個(gè) CPU 高速緩存中的數(shù)據(jù)是相互獨(dú)立,它們無(wú)法感知到對(duì)方的變化。

到這里,就產(chǎn)生了第一個(gè)問題:硬件層面上,由于多 CPU 的存在,以及加入 CPU 高速緩存,導(dǎo)致的數(shù)據(jù)一致性問題。

要注意的是,這個(gè)問題是硬件層面上的問題。只要使用了多 CPU 并且 CPU 有高速緩存,那就會(huì)遇到這個(gè)問題。對(duì)于生產(chǎn)該 CPU 的廠商,就需要去解決這個(gè)問題,這與具體操作系統(tǒng)無(wú)關(guān),也與編程語(yǔ)言無(wú)關(guān)。

那么如何解決這個(gè)問題呢?答案是:緩存一致性協(xié)議。

圖片

加入緩存一致性協(xié)議

所謂的緩存一致性協(xié)議,指的是在 CPU 高速緩存與主內(nèi)存交互的時(shí)候,遵守特定的規(guī)則,這樣就可以避免數(shù)據(jù)一致性問題了。

在不同的 CPU 中,會(huì)使用不同的緩存一致性協(xié)議。例如 MESI 協(xié)議用于奔騰系列的 CPU 中,而 MOSEI 協(xié)議則用于 AMD 系列 CPU 中,Intel 的 core i7 處理器使用 MESIF 協(xié)議。在這里我們介紹最為常見的一種:MESI 數(shù)據(jù)一致性協(xié)議。

在 MESI 協(xié)議中,每個(gè)緩存可能有有 4 個(gè)狀態(tài),它們分別是:

  • M (Modified):這行數(shù)據(jù)有效,數(shù)據(jù)被修改了,和內(nèi)存中的數(shù)據(jù)不一致,數(shù)據(jù)只存在于本 Cache 中。
  • E (Exclusive):這行數(shù)據(jù)有效,數(shù)據(jù)和內(nèi)存中的數(shù)據(jù)一致,數(shù)據(jù)只存在于本 Cache 中。
  • S (Shared):這行數(shù)據(jù)有效,數(shù)據(jù)和內(nèi)存中的數(shù)據(jù)一致,數(shù)據(jù)存在于很多 Cache 中。
  • I (Invalid):這行數(shù)據(jù)無(wú)效。

那么在 MESI 協(xié)議的作用下,我們上面的線程執(zhí)行過(guò)程就變?yōu)椋?/p>

  • 線程 A 分配到 CPU0 執(zhí)行,這時(shí)候讀取 i 的值為 0,存到 CPU0 的高速緩存中。
  • 線程 B 分配到 CPU1 執(zhí)行,這時(shí)候讀取 i 的值為 0,存到 CPU1 的高速緩存中。
  • CPU0 進(jìn)行運(yùn)算,得出結(jié)果 10,運(yùn)算結(jié)束,寫回內(nèi)存,此時(shí)內(nèi)存 i 的值為 10。同時(shí)通過(guò)消息的方式告訴其他持有 i 變量的 CPU 緩存,將這個(gè)緩存的狀態(tài)值為 Invalid。
  • CPU1 進(jìn)行運(yùn)算,從 CPU 緩存取出值,但是發(fā)現(xiàn)這個(gè)緩存值被置為 Invalid 了。于是重新去內(nèi)存中讀取,讀取到 10 這個(gè)值放入 CPU 緩存。
  • CPU1 進(jìn)行運(yùn)算,得出結(jié)果 20,運(yùn)算結(jié)束,寫回內(nèi)存,此時(shí)內(nèi)存 i 的值為 20。

從上面的例子,我們可以知道 MESI 緩存一致性協(xié)議,本質(zhì)上是定義了一些內(nèi)存狀態(tài),然后通過(guò)消息的方式通知其他 CPU 高速緩存,從而解決了數(shù)據(jù)一致性的問題。

從操作系統(tǒng)說(shuō)起

操作系統(tǒng),它屏蔽了底層硬件的操作細(xì)節(jié),將各種硬件資源虛擬化,方便我們進(jìn)行上層軟件的開發(fā)。在我們開發(fā)應(yīng)用軟件的時(shí)候,我們不需要直接與硬件進(jìn)行交互,只需要和操作系統(tǒng)交互即可。

既然如此,那么操作系統(tǒng)就需要將硬件進(jìn)行封裝,然后抽象出一些概念,方便上層應(yīng)用使用。于是 CPU 時(shí)間片、內(nèi)核態(tài)、用戶態(tài)等概念也誕生了。

前面我們說(shuō)到 CPU 與內(nèi)存之間會(huì)存在緩存一致性問題,那操作系統(tǒng)抽象出來(lái)的 CPU 與內(nèi)存也會(huì)面臨這樣的問題。因此,操作系統(tǒng)層面也需要去解決同樣的問題。所以,對(duì)于任何一個(gè)系統(tǒng)來(lái)說(shuō),它們都需要去解決這樣一個(gè)問題。

我們把在特定的操作協(xié)議下,對(duì)特定內(nèi)存或高速緩存進(jìn)行讀寫訪問的過(guò)程進(jìn)行抽象,得到的就是內(nèi)存模型了。 無(wú)論是 Windows 系統(tǒng),還是 Linux 系統(tǒng),它們都有特定的內(nèi)存模型。

Java 語(yǔ)言是建立在操作系統(tǒng)上層的高級(jí)語(yǔ)言,它只能與操作系統(tǒng)進(jìn)行交互,而不與硬件進(jìn)行交互。與操作系統(tǒng)相對(duì)于硬件類似,操作系統(tǒng)需要抽象出內(nèi)存模型,那么 Java 語(yǔ)言也需要抽象出相對(duì)于操作系統(tǒng)的內(nèi)存模型。

一般來(lái)說(shuō),編程語(yǔ)言也可以直接復(fù)用操作系統(tǒng)層面的內(nèi)存模型,例如:C++ 語(yǔ)言就是這么做的。但由于不同操作系統(tǒng)的內(nèi)存模型不同,有可能導(dǎo)致程序在一套平臺(tái)上并發(fā)完全正常,而在另外一套平臺(tái)上并發(fā)訪問卻經(jīng)常出錯(cuò)。因此在某些場(chǎng)景下,就必須針對(duì)不同的平臺(tái)來(lái)編寫程序。

而我們都知道 Java 的最大特點(diǎn)是「Write Once, Run Anywhere」,即一次編譯哪里都可以運(yùn)行。而為了達(dá)到這樣一個(gè)目標(biāo),Java 語(yǔ)言就必須在各個(gè)操作系統(tǒng)的基礎(chǔ)上進(jìn)一步抽象,建立起一套對(duì)內(nèi)存或高速緩存的讀寫訪問抽象標(biāo)準(zhǔn)。這樣就可以保證無(wú)論在哪個(gè)操作系統(tǒng),只要遵循了這個(gè)規(guī)范,都能保證并發(fā)訪問是正常的。

圖片

Java 內(nèi)存模型 - 不同層面抽象及方案

Java 內(nèi)存模型

經(jīng)過(guò)了前面的鋪墊,相信你已經(jīng)明白了為什么要有 Java 內(nèi)存模型,以及 Java 內(nèi)存模型是什么,有了一個(gè)感性的理解。這里我們?cè)俳o Java 內(nèi)存模型下一個(gè)較為準(zhǔn)確的定義。

Java 內(nèi)存模型(Java Memory Model,JMM)用于屏蔽各種硬件和操作系統(tǒng)的內(nèi)存訪問差異,以實(shí)現(xiàn)讓 Java 程序在各種平臺(tái)都能達(dá)到一致的內(nèi)存訪問效果。

Java 內(nèi)存模型定義程序中各個(gè)變量的訪問規(guī)則,即在虛擬機(jī)中將變量存儲(chǔ)到內(nèi)存和從內(nèi)存中取出變量這樣的底層細(xì)節(jié)。

這里說(shuō)的變量包括了實(shí)例字段、靜態(tài)字段和構(gòu)成數(shù)組對(duì)象的元素,但不包括局部變量與方法參數(shù)。因?yàn)楹笳呤蔷€程私有的,不會(huì)被共享,自然就不會(huì)存在競(jìng)爭(zhēng)問題。

內(nèi)存模型的定義

Java 內(nèi)存模型規(guī)定所有的變量都存儲(chǔ)在主內(nèi)存中,每條線程都有自己的工作內(nèi)存。線程的工作內(nèi)存中保存了被該線程使用到的變量的主內(nèi)存副本拷貝,線程對(duì)變量的所有操作(讀取、賦值等)都必須在工作內(nèi)存中進(jìn)行,而不能直接讀寫主內(nèi)存中的變量。

不同線程之間也無(wú)法直接訪問對(duì)方工作內(nèi)存中的變量,線程間變量值的傳遞都需要通過(guò)主內(nèi)存來(lái)完成。主內(nèi)存、工作內(nèi)存、線程三者之間的關(guān)系如下圖所示。

圖片

Java 內(nèi)存模型圖解

Java 內(nèi)存模型的主內(nèi)存、工作內(nèi)存與 JVM 的堆、棧、方法區(qū),并不是同一層次的內(nèi)存劃分,兩者是沒有關(guān)聯(lián)的。如果一定要對(duì)應(yīng)一下,那么主內(nèi)存主要對(duì)應(yīng)于 Java 堆中對(duì)象實(shí)例的數(shù)據(jù)部分,而工作內(nèi)存則對(duì)應(yīng)于虛擬機(jī)棧中的部分區(qū)域。

內(nèi)存間的交互

關(guān)于主內(nèi)存與工作內(nèi)存之間具體的交互協(xié)議,即一個(gè)變量如何從主內(nèi)存拷貝到工作內(nèi)存,以及如何從工作內(nèi)存同步回主內(nèi)存的細(xì)節(jié),Java 內(nèi)存模型定義了 8 種操作來(lái)完成。虛擬機(jī)實(shí)現(xiàn)的時(shí)候必須保證下面提及的每一種操作都是原子的、不可再分的。

  • lock(鎖定):作用于主內(nèi)存的變量,它把一個(gè)變量標(biāo)識(shí)為一條線程獨(dú)占的狀態(tài)。
  • unlock(解鎖):作用于主內(nèi)存的變量,它把一個(gè)處于鎖定狀態(tài)的變量釋放出來(lá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(存儲(chǔ)):作用于工作內(nèi)存的變量,它把工作內(nèi)存中一個(gè)變量的值傳送到主內(nèi)存中,以便隨后的 write 操作使用。
  • write(寫入):作用于主內(nèi)存的變量,它把 store 操作從工作內(nèi)存中得到的變量的值放入主內(nèi)存的變量中。

如果要把一個(gè)變量從主內(nèi)存復(fù)制到工作內(nèi)存,那就要順序地執(zhí)行 read 和 load 操作,如果要把變量從工作內(nèi)存同步回主內(nèi)存,就要順序地執(zhí)行 store 和 write 操作。注意,Java 內(nèi)存模型只要求上述兩個(gè)操作必須按順序執(zhí)行,而沒有保證是連續(xù)執(zhí)行。也就是說(shuō),read 與 load 之間、store 與 write 之間是可插入其他指令的,如對(duì)主內(nèi)存中的變量 a、b 進(jìn)行訪問時(shí),一種可能出現(xiàn)順序是 read a、read b、load b、load a。

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

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

這 8 種內(nèi)存訪問操作以及上述規(guī)則限定,再加上稍后介紹的對(duì) volatile 的一些特殊規(guī)定,就已經(jīng)完全確定了 Java 程序中哪些內(nèi)存訪問操作在并發(fā)下是安全的。 

總結(jié)

這篇文章我們從底層 CPU 開始講起,一直講到操作系統(tǒng),最后講到了編程語(yǔ)言層面,讓大家能夠一環(huán)扣一環(huán)地理解,最后明白 Java 內(nèi)存模型誕生的原因(上層有數(shù)據(jù)一致性問題),以及最終要解決的問題(緩存一致性問題)。

看到這里,我們大概把為什么要有 Java 內(nèi)存模型講清楚了,也知道了 Java 內(nèi)存模型是什么。最后我們來(lái)做個(gè)總結(jié):

由于多核 CPU 和高速緩存在存在,導(dǎo)致了緩存一致性問題。這個(gè)問題屬于硬件層面上的問題,而解決辦法是各種緩存一致性協(xié)議。不同 CPU 采用的協(xié)議不同,MESI 是最經(jīng)典的一個(gè)緩存一致性協(xié)議。

操作系統(tǒng)作為對(duì)底層硬件的抽象,自然也需要解決 CPU 高速緩存與內(nèi)存之間的緩存一致性問題。各個(gè)操作系統(tǒng)都對(duì) CPU 高速緩存與緩存的讀寫訪問過(guò)程進(jìn)行抽象,最終得到的一個(gè)東西就是「內(nèi)存模型」。

Java 語(yǔ)言作為運(yùn)行在操作系統(tǒng)層面的高級(jí)語(yǔ)言,為了解決多平臺(tái)運(yùn)行的問題,在操作系統(tǒng)基礎(chǔ)上進(jìn)一步抽象,得到了 Java 語(yǔ)言層面上的內(nèi)存模型。

Java 內(nèi)存模型分為工作內(nèi)存與主內(nèi)存,每個(gè)線程都有自己的工作內(nèi)存。每個(gè)線程都不能直接與主內(nèi)存交互,只能與工作內(nèi)存交互。此外,為了保證并發(fā)編程下的數(shù)據(jù)準(zhǔn)確性,Java 內(nèi)存模型還定義了 8 個(gè)基本的原子操作,以及 8 條基本的規(guī)則。

如果 Java 程序能夠遵守 Java 內(nèi)存模型的規(guī)則,那么其寫出的程序就是并發(fā)安全的,這就是 Java 內(nèi)存模型最大的價(jià)值。

圖片深入理解 Java 內(nèi)存模型

參考資料

  • Java 內(nèi)存模型原理,你真的理解嗎?
  • 《Java 并發(fā)編程的藝術(shù)》
  • Java 并發(fā)編程實(shí)戰(zhàn) - 蓋茨等 - 微信讀書
  • Java 高并發(fā)編程詳解:深入理解并發(fā)核心庫(kù) - 汪文君 - 微信讀書
  • 操作系統(tǒng)對(duì) CPU 的控制權(quán) | 王輝的博客
  • Operating Systems: Three Easy Pieces
  • 既然 CPU 有緩存一致性協(xié)議(MESI),為什么 JMM 還需要 volatile 關(guān)鍵字?- 羅一鑫的回答 - 知乎

責(zé)任編輯:武曉燕 來(lái)源: 陳樹義
相關(guān)推薦

2023-11-05 12:05:35

JVM內(nèi)存

2015-03-24 13:28:52

Java Java Strin內(nèi)存模型

2023-09-19 22:47:39

Java內(nèi)存

2016-09-18 10:25:07

CPU分支預(yù)測(cè)模型

2021-09-08 17:42:45

JVM內(nèi)存模型

2023-10-27 07:47:58

Java語(yǔ)言順序性

2022-07-06 08:05:52

Java對(duì)象JVM

2020-11-11 08:45:48

Java

2020-06-01 21:07:33

C11C++11內(nèi)存

2020-11-04 15:35:13

Golang內(nèi)存程序員

2013-06-20 10:25:56

2021-11-26 00:00:48

JVM內(nèi)存區(qū)域

2015-12-28 11:41:57

JVM內(nèi)存區(qū)域內(nèi)存溢出

2019-05-06 14:36:48

CPULinux寄存器

2017-12-18 16:33:55

多線程對(duì)象模型

2010-06-01 15:25:27

JavaCLASSPATH

2016-12-08 15:36:59

HashMap數(shù)據(jù)結(jié)構(gòu)hash函數(shù)

2020-07-21 08:26:08

SpringSecurity過(guò)濾器

2023-10-27 07:47:37

計(jì)算機(jī)內(nèi)存模型

2022-08-21 16:52:27

Linux虛擬內(nèi)存
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號(hào)