掃二維碼與項(xiàng)目經(jīng)理溝通
我們?cè)谖⑿派?4小時(shí)期待你的聲音
解答本文疑問(wèn)/技術(shù)咨詢/運(yùn)營(yíng)咨詢/技術(shù)建議/互聯(lián)網(wǎng)交流
Java 內(nèi)存模型是并發(fā)編程的基礎(chǔ),只有對(duì) Java 內(nèi)存模型理解較為透徹,我們才能避免一些錯(cuò)誤地理解。Java 中一些高級(jí)的特性,也建立在 Java 內(nèi)存模型的基礎(chǔ)上,例如:volatile 關(guān)鍵字。

專注于為中小企業(yè)提供成都做網(wǎng)站、成都網(wǎng)站建設(shè)服務(wù),電腦端+手機(jī)端+微信端的三站合一,更高效的管理,為中小企業(yè)東港免費(fèi)做網(wǎng)站提供優(yōu)質(zhì)的服務(wù)。我們立足成都,凝聚了一批互聯(lián)網(wǎng)行業(yè)人才,有力地推動(dòng)了上1000家企業(yè)的穩(wěn)健成長(zhǎng),幫助中小企業(yè)通過(guò)網(wǎng)站建設(shè)實(shí)現(xiàn)規(guī)模擴(kuò)充和轉(zhuǎn)變。
為了讓大家能明白 Java 內(nèi)存模型存在的意義,本篇文章將從計(jì)算機(jī)硬件出發(fā),一路寫到操作系統(tǒng)、編程語(yǔ)言,一環(huán)扣一環(huán)的引出 Java 內(nèi)存模型存在的意義,讓大家對(duì) Java 內(nèi)存模型有較為深刻的理解??赐曛?,希望大家能夠明白如下幾個(gè)問(wèn)題:
我們知道計(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)了新的問(wèn)題:緩存一致性。
在多 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)致的問(wèn)題
我們舉個(gè)例子,線程 A 執(zhí)行這樣一段代碼:
i = i + 10;
線程 B 執(zhí)行這樣一段代碼:
i = i + 10;
他們的 i 都是存儲(chǔ)在內(nèi)存中共用的,初始值是 0。按照我們的設(shè)想,最終輸出的值應(yīng)該是 20 才對(duì)。但實(shí)際上有可能輸出的值是 10。下面是可能發(fā)生的一種情況:
可以看到發(fā)生錯(cuò)誤結(jié)果的主要原因是:兩個(gè) CPU 高速緩存中的數(shù)據(jù)是相互獨(dú)立,它們無(wú)法感知到對(duì)方的變化。
到這里,就產(chǎn)生了第一個(gè)問(wèn)題:硬件層面上,由于多 CPU 的存在,以及加入 CPU 高速緩存,導(dǎo)致的數(shù)據(jù)一致性問(wèn)題。
要注意的是,這個(gè)問(wèn)題是硬件層面上的問(wèn)題。只要使用了多 CPU 并且 CPU 有高速緩存,那就會(huì)遇到這個(gè)問(wèn)題。對(duì)于生產(chǎn)該 CPU 的廠商,就需要去解決這個(gè)問(wèn)題,這與具體操作系統(tǒng)無(wú)關(guān),也與編程語(yǔ)言無(wú)關(guān)。
那么如何解決這個(gè)問(wèn)題呢?答案是:緩存一致性協(xié)議。
加入緩存一致性協(xié)議
所謂的緩存一致性協(xié)議,指的是在 CPU 高速緩存與主內(nèi)存交互的時(shí)候,遵守特定的規(guī)則,這樣就可以避免數(shù)據(jù)一致性問(wèn)題了。
在不同的 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),它們分別是:
那么在 MESI 協(xié)議的作用下,我們上面的線程執(zhí)行過(guò)程就變?yōu)椋?/p>
從上面的例子,我們可以知道 MESI 緩存一致性協(xié)議,本質(zhì)上是定義了一些內(nèi)存狀態(tài),然后通過(guò)消息的方式通知其他 CPU 高速緩存,從而解決了數(shù)據(jù)一致性的問(wèn)題。
操作系統(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ì)存在緩存一致性問(wèn)題,那操作系統(tǒng)抽象出來(lái)的 CPU 與內(nèi)存也會(huì)面臨這樣的問(wèn)題。因此,操作系統(tǒng)層面也需要去解決同樣的問(wèn)題。所以,對(duì)于任何一個(gè)系統(tǒng)來(lái)說(shuō),它們都需要去解決這樣一個(gè)問(wèn)題。
我們把在特定的操作協(xié)議下,對(duì)特定內(nèi)存或高速緩存進(jìn)行讀寫訪問(wè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ā)訪問(wèn)卻經(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)存或高速緩存的讀寫訪問(wèn)抽象標(biāo)準(zhǔn)。這樣就可以保證無(wú)論在哪個(gè)操作系統(tǒng),只要遵循了這個(gè)規(guī)范,都能保證并發(fā)訪問(wèn)是正常的。
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)存訪問(wèn)差異,以實(shí)現(xiàn)讓 Java 程序在各種平臺(tái)都能達(dá)到一致的內(nèi)存訪問(wèn)效果。
Java 內(nèi)存模型定義程序中各個(gè)變量的訪問(wèn)規(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)問(wèn)題。
內(nèi)存模型的定義
Java 內(nèi)存模型規(guī)定所有的變量都存儲(chǔ)在主內(nèi)存中,每條線程都有自己的工作內(nèi)存。線程的工作內(nèi)存中保存了被該線程使用到的變量的主內(nèi)存副本拷貝,線程對(duì)變量的所有操作(讀取、賦值等)都必須在工作內(nèi)存中進(jìn)行,而不能直接讀寫主內(nèi)存中的變量。
不同線程之間也無(wú)法直接訪問(wèn)對(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í)候必須保證下面提及的每一種操作都是原子的、不可再分的。
如果要把一個(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)行訪問(wèn)時(shí),一種可能出現(xiàn)順序是 read a、read b、load b、load a。
此外,Java 內(nèi)存模型還規(guī)定上述 8 種基本操作時(shí)必須滿足如下規(guī)則:
這 8 種內(nèi)存訪問(wèn)操作以及上述規(guī)則限定,再加上稍后介紹的對(duì) volatile 的一些特殊規(guī)定,就已經(jīng)完全確定了 Java 程序中哪些內(nèi)存訪問(wèn)操作在并發(fā)下是安全的。
這篇文章我們從底層 CPU 開始講起,一直講到操作系統(tǒng),最后講到了編程語(yǔ)言層面,讓大家能夠一環(huán)扣一環(huán)地理解,最后明白 Java 內(nèi)存模型誕生的原因(上層有數(shù)據(jù)一致性問(wèn)題),以及最終要解決的問(wèn)題(緩存一致性問(wèn)題)。
看到這里,我們大概把為什么要有 Java 內(nèi)存模型講清楚了,也知道了 Java 內(nèi)存模型是什么。最后我們來(lái)做個(gè)總結(jié):
由于多核 CPU 和高速緩存在存在,導(dǎo)致了緩存一致性問(wèn)題。這個(gè)問(wèn)題屬于硬件層面上的問(wèn)題,而解決辦法是各種緩存一致性協(xié)議。不同 CPU 采用的協(xié)議不同,MESI 是最經(jīng)典的一個(gè)緩存一致性協(xié)議。
操作系統(tǒng)作為對(duì)底層硬件的抽象,自然也需要解決 CPU 高速緩存與內(nèi)存之間的緩存一致性問(wèn)題。各個(gè)操作系統(tǒng)都對(duì) CPU 高速緩存與緩存的讀寫訪問(wèn)過(guò)程進(jìn)行抽象,最終得到的一個(gè)東西就是「內(nèi)存模型」。
Java 語(yǔ)言作為運(yùn)行在操作系統(tǒng)層面的高級(jí)語(yǔ)言,為了解決多平臺(tái)運(yùn)行的問(wè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)存模型

我們?cè)谖⑿派?4小時(shí)期待你的聲音
解答本文疑問(wèn)/技術(shù)咨詢/運(yùn)營(yíng)咨詢/技術(shù)建議/互聯(lián)網(wǎng)交流