溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點(diǎn)擊 登錄注冊 即表示同意《億速云用戶服務(wù)條款》

Java內(nèi)存模型原理是什么

發(fā)布時(shí)間:2022-01-07 20:12:47 來源:億速云 閱讀:100 作者:iii 欄目:編程語言

這篇文章主要介紹“Java內(nèi)存模型原理是什么”的相關(guān)知識,小編通過實(shí)際案例向大家展示操作過程,操作方法簡單快捷,實(shí)用性強(qiáng),希望這篇“Java內(nèi)存模型原理是什么”文章能幫助大家解決問題。

內(nèi)部原理

JVM 中試圖定義一種 JMM 來屏蔽各種硬件和操作系統(tǒng)的內(nèi)存訪問差異,以實(shí)現(xiàn)讓 Java 程序在各種平臺(tái)下都能達(dá)到一致的內(nèi)存訪問效果。

JMM 的主要目標(biāo)是定義程序中各個(gè)變量的訪問規(guī)則,即在虛擬機(jī)中將變量存儲(chǔ)到內(nèi)存和從內(nèi)存中取出變量這樣的底層細(xì)節(jié)。此處的變量與 Java 編程中的變量有所區(qū)別,它包括了實(shí)例字段、靜態(tài)字段和構(gòu)成數(shù)組對象的元素,但不包括局部變量與方法參數(shù),因?yàn)楹笳呤蔷€程私有的,不會(huì)被共享,自然就不會(huì)存在競爭問題。為了獲得較好的執(zhí)行效能,Java 內(nèi)存模型并沒有限制執(zhí)行引擎使用處理器的特定寄存器或緩存來和主存進(jìn)行交互,也沒有限制即使編譯器進(jìn)行調(diào)整代碼執(zhí)行順序這類優(yōu)化措施。

JMM 是圍繞著在并發(fā)過程中如何處理原子性、可見性和有序性這 3 個(gè)特征來建立的。

JMM 是通過各種操作來定義的,包括對變量的讀寫操作,監(jiān)視器的加鎖和釋放操作,以及線程的啟動(dòng)和合并操作。

內(nèi)存模型結(jié)構(gòu)

Java 內(nèi)存模型把 Java 虛擬機(jī)內(nèi)部劃分為線程棧和堆。

線程棧

每一個(gè)運(yùn)行在 Java 虛擬機(jī)里的線程都擁有自己的線程棧。這個(gè)線程棧包含了這個(gè)線程調(diào)用的方法當(dāng)前執(zhí)行點(diǎn)相關(guān)的信息。一個(gè)線程僅能訪問自己的線程棧。一個(gè)線程創(chuàng)建的本地變量對其它線程不可見,僅自己可見。即使兩個(gè)線程執(zhí)行同樣的代碼,這兩個(gè)線程任然在在自己的線程棧中的代碼來創(chuàng)建本地變量。因此,每個(gè)線程擁有每個(gè)本地變量的獨(dú)有版本。

所有原始類型的本地變量都存放在線程棧上,因此對其它線程不可見。一個(gè)線程可能向另一個(gè)線程傳遞一個(gè)原始類型變量的拷貝,但是它不能共享這個(gè)原始類型變量自身。

堆上包含在 Java 程序中創(chuàng)建的所有對象,無論是哪一個(gè)對象創(chuàng)建的。這包括原始類型的對象版本。如果一個(gè)對象被創(chuàng)建然后賦值給一個(gè)局部變量,或者用來作為另一個(gè)對象的成員變量,這個(gè)對象任然是存放在堆上。

  • 一個(gè)本地變量可能是原始類型,在這種情況下,它總是在線程棧上。

  • 一個(gè)本地變量也可能是指向一個(gè)對象的一個(gè)引用。在這種情況下,引用(這個(gè)本地變量)存放在線程棧上,但是對象本身存放在堆上。

  • 一個(gè)對象可能包含方法,這些方法可能包含本地變量。這些本地變量任然存放在線程棧上,即使這些方法所屬的對象存放在堆上。

  • 一個(gè)對象的成員變量可能隨著這個(gè)對象自身存放在堆上。不管這個(gè)成員變量是原始類型還是引用類型。

  • 靜態(tài)成員變量跟隨著類定義一起也存放在堆上。

  • 存放在堆上的對象可以被所有持有對這個(gè)對象引用的線程訪問。當(dāng)一個(gè)線程可以訪問一個(gè)對象時(shí),它也可以訪問這個(gè)對象的成員變量。如果兩個(gè)線程同時(shí)調(diào)用同一個(gè)對象上的同一個(gè)方法,它們將會(huì)都訪問這個(gè)對象的成員變量,但是每一個(gè)線程都擁有這個(gè)本地變量的私有拷貝。

Java內(nèi)存模型原理是什么

硬件內(nèi)存架構(gòu)

現(xiàn)代硬件內(nèi)存模型與 Java 內(nèi)存模型有一些不同。理解內(nèi)存模型架構(gòu)以及 Java 內(nèi)存模型如何與它協(xié)同工作也是非常重要的。這部分描述了通用的硬件內(nèi)存架構(gòu),下面的部分將會(huì)描述 Java 內(nèi)存是如何與它“聯(lián)手”工作的。

Java內(nèi)存模型原理是什么

一個(gè)現(xiàn)代計(jì)算機(jī)通常由兩個(gè)或者多個(gè) CPU。其中一些 CPU 還有多核。從這一點(diǎn)可以看出,在一個(gè)有兩個(gè)或者多個(gè) CPU 的現(xiàn)代計(jì)算機(jī)上同時(shí)運(yùn)行多個(gè)線程是可能的。每個(gè) CPU 在某一時(shí)刻運(yùn)行一個(gè)線程是沒有問題的。這意味著,如果你的 Java 程序是多線程的,在你的 Java 程序中每個(gè) CPU 上一個(gè)線程可能同時(shí)(并發(fā))執(zhí)行。

每個(gè) CPU 都包含一系列的寄存器,它們是 CPU 內(nèi)內(nèi)存的基礎(chǔ)。CPU 在寄存器上執(zhí)行操作的速度遠(yuǎn)大于在主存上執(zhí)行的速度。這是因?yàn)?CPU 訪問寄存器的速度遠(yuǎn)大于主存。

每個(gè) CPU 可能還有一個(gè) CPU 緩存層。實(shí)際上,絕大多數(shù)的現(xiàn)代 CPU 都有一定大小的緩存層。CPU 訪問緩存層的速度快于訪問主存的速度,但通常比訪問內(nèi)部寄存器的速度還要慢一點(diǎn)。一些 CPU 還有多層緩存,但這些對理解 Java 內(nèi)存模型如何和內(nèi)存交互不是那么重要。只要知道 CPU 中可以有一個(gè)緩存層就可以了。

一個(gè)計(jì)算機(jī)還包含一個(gè)主存。所有的 CPU 都可以訪問主存。主存通常比 CPU 中的緩存大得多。

通常情況下,當(dāng)一個(gè) CPU 需要讀取主存時(shí),它會(huì)將主存的部分讀到 CPU 緩存中。它甚至可能將緩存中的部分內(nèi)容讀到它的內(nèi)部寄存器中,然后在寄存器中執(zhí)行操作。當(dāng) CPU 需要將結(jié)果寫回到主存中去時(shí),它會(huì)將內(nèi)部寄存器的值刷新到緩存中,然后在某個(gè)時(shí)間點(diǎn)將值刷新回主存。

當(dāng) CPU 需要在緩存層存放一些東西的時(shí)候,存放在緩存中的內(nèi)容通常會(huì)被刷新回主存。CPU 緩存可以在某一時(shí)刻將數(shù)據(jù)局部寫到它的內(nèi)存中,和在某一時(shí)刻局部刷新它的內(nèi)存。它不會(huì)再某一時(shí)刻讀/寫整個(gè)緩存。通常,在一個(gè)被稱作“cache lines”的更小的內(nèi)存塊中緩存被更新。一個(gè)或者多個(gè)緩存行可能被讀到緩存,一個(gè)或者多個(gè)緩存行可能再被刷新回主存。

JMM 和硬件內(nèi)存架構(gòu)之間的橋接

上面已經(jīng)提到,Java 內(nèi)存模型與硬件內(nèi)存架構(gòu)之間存在差異。硬件內(nèi)存架構(gòu)沒有區(qū)分線程棧和堆。對于硬件,所有的線程棧和堆都分布在主內(nèi)中。部分線程棧和堆可能有時(shí)候會(huì)出現(xiàn)在 CPU 緩存中和 CPU 內(nèi)部的寄存器中。如下圖所示:

當(dāng)對象和變量被存放在計(jì)算機(jī)中各種不同的內(nèi)存區(qū)域中時(shí),就可能會(huì)出現(xiàn)一些具體的問題。主要包括如下兩個(gè)方面:

  • 線程對共享變量修改的可見性

  • 當(dāng)讀,寫和檢查共享變量時(shí)出現(xiàn) race conditions

Java內(nèi)存模型原理是什么

共享對象可見性

如果兩個(gè)或者更多的線程在沒有正確的使用 volatile 聲明或者同步的情況下共享一個(gè)對象,一個(gè)線程更新這個(gè)共享對象可能對其它線程來說是不接見的。

想象一下,共享對象被初始化在主存中。跑在 CPU 上的一個(gè)線程將這個(gè)共享對象讀到 CPU 緩存中。然后修改了這個(gè)對象。只要 CPU 緩存沒有被刷新會(huì)主存,對象修改后的版本對跑在其它 CPU 上的線程都是不可見的。這種方式可能導(dǎo)致每個(gè)線程擁有這個(gè)共享對象的私有拷貝,每個(gè)拷貝停留在不同的 CPU 緩存中。

上圖示意了這種情形。跑在左邊 CPU 的線程拷貝這個(gè)共享對象到它的 CPU 緩存中,然后將 count 變量的值修改為 2。這個(gè)修改對跑在右邊 CPU 上的其它線程是不可見的,因?yàn)樾薷暮蟮?count 的值還沒有被刷新回主存中去。

解決這個(gè)問題你可以使用 Java 中的 volatile 關(guān)鍵字。volatile 關(guān)鍵字可以保證直接從主存中讀取一個(gè)變量,如果這個(gè)變量被修改后,總是會(huì)被寫回到主存中去。

競態(tài)條件

如果兩個(gè)或者更多的線程共享一個(gè)對象,多個(gè)線程在這個(gè)共享對象上更新變量,就有可能發(fā)生 race conditions。

想象一下,如果線程 A 讀一個(gè)共享對象的變量 count 到它的 CPU 緩存中。再想象一下,線程 B 也做了同樣的事情,但是往一個(gè)不同的 CPU 緩存中?,F(xiàn)在線程 A 將 count 加 1,線程 B 也做了同樣的事情。現(xiàn)在 count 已經(jīng)被增在了兩個(gè),每個(gè) CPU 緩存中一次。

如果這些增加操作被順序的執(zhí)行,變量 count 應(yīng)該被增加兩次,然后原值+2 被寫回到主存中去。

然而,兩次增加都是在沒有適當(dāng)?shù)耐较虏l(fā)執(zhí)行的。無論是線程 A 還是線程 B 將 count 修改后的版本寫回到主存中取,修改后的值僅會(huì)被原值大 1,盡管增加了兩次。

解決這個(gè)問題可以使用 Java 同步塊。一個(gè)同步塊可以保證在同一時(shí)刻僅有一個(gè)線程可以進(jìn)入代碼的臨界區(qū)。同步塊還可以保證代碼塊中所有被訪問的變量將會(huì)從主存中讀入,當(dāng)線程退出同步代碼塊時(shí),所有被更新的變量都會(huì)被刷新回主存中去,不管這個(gè)變量是否被聲明為 volatile。

Happens-Before

JMM 為程序中所有的操作定義了一個(gè)偏序關(guān)系,稱之為 Happens-Before。

  • 程序順序規(guī)則:如果程序中操作 A 在操作 B 之前,那么在線程中操作 A 將在操作 B 之前執(zhí)行。

  • 監(jiān)視器鎖規(guī)則:在監(jiān)視器鎖上的解鎖操作必須在同一個(gè)監(jiān)視器鎖上的加鎖操作之前執(zhí)行。

  • volatile 變量規(guī)則:對 volatile 變量的寫入操作必須在對該變量的讀操作之前執(zhí)行。

  • 線程啟動(dòng)規(guī)則:在線程上對 Thread.start 的調(diào)用必須在該線程中執(zhí)行任何操作之前執(zhí)行。

  • 線程結(jié)束規(guī)則:線程中的任何操作都必須在其他線程檢測到該線程已經(jīng)結(jié)束之前執(zhí)行,或者從 Thread.join 中成功返回,或者在調(diào)用 Thread.isAlive 時(shí)返回 false。

  • 中斷規(guī)則:當(dāng)一個(gè)線程在另一個(gè)線程上調(diào)用 interrupt 時(shí),必須在被中斷線程檢測到 interrupt 調(diào)用之前執(zhí)行(通過拋出 InterruptException,或者調(diào)用 isInterrupted 和 interrupted)。

  • 終結(jié)器規(guī)則:對象的構(gòu)造函數(shù)必須在啟動(dòng)該對象的終結(jié)器之前執(zhí)行完成。

  • 傳遞性:如果操作 A 在操作 B 之前執(zhí)行,并且操作 B 在操作 C 之前執(zhí)行,那么操作 A 必須在操作 C 之前執(zhí)行。

關(guān)于“Java內(nèi)存模型原理是什么”的內(nèi)容就介紹到這里了,感謝大家的閱讀。如果想了解更多行業(yè)相關(guān)的知識,可以關(guān)注億速云行業(yè)資訊頻道,小編每天都會(huì)為大家更新不同的知識點(diǎn)。

向AI問一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI