成人免费xxxxx在线视频软件_久久精品久久久_亚洲国产精品久久久_天天色天天色_亚洲人成一区_欧美一级欧美三级在线观看

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

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

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

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

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

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

從 CPU 說起

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

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

圖片

新增 CPU 高速緩存

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

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

圖片

多核 CPU 及高速緩存導致的問題

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

i = i + 10;

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

i = i + 10;

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

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

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

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

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

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

圖片

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

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

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

在 MESI 協(xié)議中,每個緩存可能有有 4 個狀態(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ù)無效。

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

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

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

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

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

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

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

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

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

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

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

圖片

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

Java 內(nèi)存模型

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

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

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

這里說的變量包括了實例字段、靜態(tài)字段和構成數(shù)組對象的元素,但不包括局部變量與方法參數(shù)。因為后者是線程私有的,不會被共享,自然就不會存在競爭問題。

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

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

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

圖片

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

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

內(nèi)存間的交互

關于主內(nèi)存與工作內(nèi)存之間具體的交互協(xié)議,即一個變量如何從主內(nèi)存拷貝到工作內(nèi)存,以及如何從工作內(nèi)存同步回主內(nèi)存的細節(jié),Java 內(nèi)存模型定義了 8 種操作來完成。虛擬機實現(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)存,那就要順序地執(zhí)行 read 和 load 操作,如果要把變量從工作內(nèi)存同步回主內(nèi)存,就要順序地執(zhí)行 store 和 write 操作。注意,Java 內(nèi)存模型只要求上述兩個操作必須按順序執(zhí)行,而沒有保證是連續(xù)執(zhí)行。也就是說,read 與 load 之間、store 與 write 之間是可插入其他指令的,如對主內(nèi)存中的變量 a、b 進行訪問時,一種可能出現(xiàn)順序是 read a、read b、load b、load a。

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

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

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

總結

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

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

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

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

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

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

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

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

參考資料

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

責任編輯:武曉燕 來源: 陳樹義
相關推薦

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分支預測模型

2023-10-27 07:47:58

Java語言順序性

2022-07-06 08:05:52

Java對象JVM

2021-09-08 17:42:45

JVM內(nèi)存模型

2020-11-11 08:45:48

Java

2020-11-04 15:35:13

Golang內(nèi)存程序員

2013-06-20 10:25:56

2020-06-01 21:07:33

C11C++11內(nèi)存

2019-05-06 14:36:48

CPULinux寄存器

2021-11-26 00:00:48

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

2017-12-18 16:33:55

多線程對象模型

2015-12-28 11:41:57

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

2010-06-01 15:25:27

JavaCLASSPATH

2016-12-08 15:36:59

HashMap數(shù)據(jù)結構hash函數(shù)

2020-07-21 08:26:08

SpringSecurity過濾器

2023-10-27 07:47:37

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

2022-08-21 16:52:27

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

51CTO技術棧公眾號

主站蜘蛛池模板: 国产一区免费视频 | 亚洲国产精品成人综合久久久 | 久在线视频播放免费视频 | 欧美黑人又粗大 | 日韩精品成人 | 成人动慢| 欧美一区视频在线 | 国产在线一区二区三区 | 伊人伊人伊人 | 久久国产一区二区三区 | 日韩欧美在线观看 | 网站一区二区三区 | 一级a性色生活片久久毛片 午夜精品在线观看 | 日韩视频免费 | 激情五月婷婷丁香 | 91麻豆精品国产91久久久资源速度 | 久久久久亚洲精品中文字幕 | 国产乡下妇女做爰 | 午夜视频一区二区 | www.日本国产 | 一二区成人影院电影网 | 亚洲成人一二三 | 99精品国产一区二区三区 | 在线黄 | 免费人成在线观看网站 | 中文字幕免费 | 久久一起草 | 99热播精品 | 天天想天天干 | 久久国产精品-国产精品 | 久久午夜视频 | 蜜桃一区二区三区 | a国产一区二区免费入口 | 黑人精品 | 欧美日韩国产精品一区二区 | 亚洲国产成人精品女人久久久 | 国产精品久久久久久久久动漫 | 免费v片在线观看 | 日本精品在线一区 | 福利片在线观看 | 国产精品污www一区二区三区 |