CPU 緩存模型
為什么要弄一個 CPU 高速緩存(CPU Cauche)呢?
類比我們開發(fā)網(wǎng)站后臺系統(tǒng)使用的緩存(比如 Redis)是為了解決程序處理速度和訪問常規(guī)關(guān)系型數(shù)據(jù)庫速度不對等的問題。 CPU 緩存則是為了解決 CPU 處理速度和內(nèi)存處理速度不對等的問題。
我們甚至可以把內(nèi)存可以看作外存的高速緩存,程序運行的時候我們把外存的數(shù)據(jù)復(fù)制到內(nèi)存,由于內(nèi)存的處理速度遠(yuǎn)遠(yuǎn)高于外存,這樣提高了處理速度。
高速緩沖存儲器Cache是位于CPU與內(nèi)存之間的臨時存儲器,它的容量比內(nèi)存小但交換速度快。
CPU是計算機(jī)的大腦,是負(fù)責(zé)執(zhí)行指令的;自身的頻率和指令執(zhí)行的速度非??欤幻雸?zhí)行的指令大概10^9
級別的;內(nèi)存的的速度要比CPU慢上好幾個級別,每秒處理的速度大概是10^6的級別的。
如果CPU要頻繁的訪問主內(nèi)存的話,每次都需要等待很長的時間,執(zhí)行性能就會低,大部分時間都在等待主內(nèi)存返回數(shù)據(jù),沒有發(fā)揮出CPU的性能。
總結(jié):CPU Cache 緩存的是內(nèi)存數(shù)據(jù),用于解決 CPU處理速度與內(nèi)存讀寫速度不匹配的矛盾;內(nèi)存緩存的是硬盤數(shù)據(jù),用于解決硬盤訪問速度過慢的問題。
如下圖所示:
我們現(xiàn)在用的 Intel CPU,通常都是多核的的。每一個 CPU 核里面,都有獨立屬于自己的L1、L2 的 Cache,然后再有多個 CPU 核共用的 L3 的 Cache、主內(nèi)存。
CPU Cache 的工作方式: 先復(fù)制一份數(shù)據(jù)到 CPU Cache 中,當(dāng) CPU 需要用到的時候就可以直接從 CPU Cache 中讀取數(shù)據(jù),當(dāng)運算完成后,再將運算得到的數(shù)據(jù)寫回 Main Memory 中。但是,這樣存在 內(nèi)存緩存不一致性的問題 !比如我執(zhí)行一個 i++ 操作的話,如果兩個線程同時執(zhí)行的話,假設(shè)兩個線程從 CPU Cache 中讀取的 i=1,兩個線程做了 1++ 運算完之后再寫回 Main Memory 之后 i=2,而正確結(jié)果應(yīng)該是 i=3。
CPU 為了解決內(nèi)存緩存不一致性問題可以通過制定緩存一致協(xié)議(比如 MESI 協(xié)議)或者其他手段來解決。 這個緩存一致性協(xié)議指的是在 CPU 高速緩存與主內(nèi)存交互的時候需要遵守的原則和規(guī)范。不同的 CPU 中,使用的緩存一致性協(xié)議通常也會有所不同。
我們的程序運行在操作系統(tǒng)之上,操作系統(tǒng)屏蔽了底層硬件的操作細(xì)節(jié),將各種硬件資源虛擬化。于是,操作系統(tǒng)也就同樣需要解決內(nèi)存緩存不一致性問題。
操作系統(tǒng)通過 內(nèi)存模型(Memory Model) 定義一系列規(guī)范來解決這個問題。無論是 Windows 系統(tǒng),還是 Linux 系統(tǒng),它們都有特定的內(nèi)存模型。
CPU Cache 是由寄存器組成的嗎
不,CPU Cache并不是由寄存器組成的。
CPU Cache是一種用于提高處理器讀寫速度的高速緩存存儲器。它是位于CPU核心和主存(RAM)之間的一層存儲器級別。
寄存器是CPU內(nèi)部的最快和最小的存儲器,用于存儲CPU指令、數(shù)據(jù)和臨時計算結(jié)果等。而CPU Cache是位于寄存器和主存之間的一種中間級別的存儲器,它的容量比寄存器大但比主存小,速度介于寄存器與主存之間。
Cache的作用是緩存最頻繁使用的指令和數(shù)據(jù),以減少CPU對主存的訪問時間,提高處理器的執(zhí)行效率。當(dāng)CPU需要訪問內(nèi)存時,它首先會在Cache中搜索所需的數(shù)據(jù)。如果Cache中存在該數(shù)據(jù),就可以直接從Cache中讀取,避免了訪問主存的延遲。如果Cache中沒有需要的數(shù)據(jù),CPU會從主存中讀取數(shù)據(jù),并將從主存中讀取的數(shù)據(jù)緩存到Cache中,以備后續(xù)使用。
因此,CPU Cache是一層獨立于寄存器的存儲器級別,它通過局部性原理和緩存替換策略等機(jī)制,提供了更快的數(shù)據(jù)訪問速度,從而提升了CPU的整體性能。
指令重排序
計算機(jī)在執(zhí)行程序時候,為了提高代碼、指令的執(zhí)行效率,編譯器和處理器會對指令進(jìn)行重新排序,一般分為編譯器對于指令的重新排序、指令并行之間的優(yōu)化、以及內(nèi)存指令的優(yōu)化。
這么多優(yōu)化都是保證在單線程的情況下,執(zhí)行的結(jié)果是不變的,下圖就是描述整個的指令重排的優(yōu)化的過程:
什么是指令重排序? 簡單來說就是系統(tǒng)在執(zhí)行代碼的時候并不一定是按照你寫的代碼的順序依次執(zhí)行。
-
編譯器優(yōu)化:編譯器在生成目標(biāo)代碼的過程中會對指令進(jìn)行重新排序,以提高程序執(zhí)行效率。編譯器優(yōu)化技術(shù)包括常量傳播、循環(huán)展開、代碼內(nèi)聯(lián)、函數(shù)內(nèi)散列等。這些優(yōu)化技術(shù)能夠減少指令的數(shù)目,提高指令級并行性,從而提高程序的執(zhí)行速度。
-
指令級并行優(yōu)化:處理器在執(zhí)行指令的過程中,可以對指令進(jìn)行重排序,以充分利用處理器的特性和資源,提高指令級并行性。處理器可以采用亂序執(zhí)行(Out-of-Order Execution)技術(shù),在保證程序的語義正確性的前提下,重新調(diào)度指令的執(zhí)行順序,以最大程度地提高指令的并行度和執(zhí)行效率。
-
內(nèi)存指令優(yōu)化:內(nèi)存訪問是計算機(jī)中常見的瓶頸之一,處理器和編譯器會采取優(yōu)化技術(shù)來降低內(nèi)存訪問的開銷。例如,編譯器可以對內(nèi)存訪問模式進(jìn)行分析和優(yōu)化,以減少不必要的內(nèi)存訪問。處理器可以采用高速緩存(Cache)來緩存最近訪問的數(shù)據(jù),以減少對主存的訪問時間。
這些優(yōu)化技術(shù)在保持程序正確性的前提下,都是為了提高程序的執(zhí)行效率和性能。它們能夠充分利用處理器的特性和資源,減少不必要的指令和內(nèi)存訪問開銷,從而提高程序的執(zhí)行速度和響應(yīng)性。
另外,內(nèi)存系統(tǒng)也會有“重排序”,但又不是真正意義上的重排序。在 JMM 里表現(xiàn)為主存和本地內(nèi)存的內(nèi)容可能不一致,進(jìn)而導(dǎo)致程序在多線程下執(zhí)行可能出現(xiàn)問題。
Java 源代碼會經(jīng)歷 編譯器優(yōu)化重排 —> 指令并行重排 —> 內(nèi)存系統(tǒng)重排 的過程,最終才變成操作系統(tǒng)可執(zhí)行的指令序列。
指令重排序可以保證串行語義一致,但是沒有義務(wù)保證多線程間的語義也一致 ,所以在多線程下,指令重排序可能會導(dǎo)致一些問題。
編譯器和處理器的指令重排序的處理方式不一樣。對于編譯器,通過禁止特定類型的編譯器重排序的方式來禁止重排序。對于處理器,通過插入內(nèi)存屏障(Memory Barrier,或有時叫做內(nèi)存柵欄,Memory Fence)的方式來禁止特定類型的處理器重排序。指令并行重排和內(nèi)存系統(tǒng)重排都屬于是處理器級別的指令重排序。
為什么需要內(nèi)存屏障:編譯器和處理器指令重排只能保證在單線程執(zhí)行下邏輯正確,在多個線程同時讀寫多個變量的情況下,如果不對指令重排作出一定
限制,代碼的執(zhí)行結(jié)果會根據(jù)指令重排后的順序產(chǎn)生不同的結(jié)果。指令重排后的順序每次執(zhí)行時都可能不一樣,顯然我們希望我們的代碼執(zhí)行結(jié)果與代碼
順序是邏輯一致的(可能不太準(zhǔn)確),所以我們需要內(nèi)存屏障。
內(nèi)存屏障
內(nèi)存屏障(Memory Barrier)是一種計算機(jī)硬件或軟件機(jī)制,用于控制計算機(jī)處理器和內(nèi)存之間的數(shù)據(jù)同步和可見性。它負(fù)責(zé)確保在多線程或多核處理器系統(tǒng)中,不同線程之間的內(nèi)存操作具有正確的順序和一致的結(jié)果。
內(nèi)存屏障有以下幾個主要作用:
-
保證內(nèi)存可見性:內(nèi)存屏障通過禁止處理器對指令的重排序以及對數(shù)據(jù)的緩存寫回,來確保數(shù)據(jù)的可見性。它可以防止在多線程場景下,某個線程對某個共享變量的修改對其他線程不可見的情況。
-
確保指令順序:內(nèi)存屏障可以防止指令亂序執(zhí)行,保證指令按照程序的原始順序執(zhí)行。它可以防止在多線程場景下,由于指令重排引起的數(shù)據(jù)一致性問題。
-
防止優(yōu)化:內(nèi)存屏障可以防止處理器對指令和數(shù)據(jù)的過度優(yōu)化,以確保程序的執(zhí)行結(jié)果符合預(yù)期。它可以防止在多線程場景下,由于指令和數(shù)據(jù)的優(yōu)化導(dǎo)致的錯誤結(jié)果。
存屏障通常包括以下幾種類型:
-
Load Barrier:用于確保一個線程讀取的數(shù)據(jù)來自于主內(nèi)存而不是本地內(nèi)存。
-
Store Barrier:用于確保一個線程寫入的數(shù)據(jù)同步到主內(nèi)存而不是本地內(nèi)存。
-
Full Barrier:包含Load Barrier和Store Barrier,用于確保一個線程讀取和寫入的數(shù)據(jù)都同步到主內(nèi)存而不是本地內(nèi)存。
-
Store-Load Barrier:用于確保一個線程寫入的數(shù)據(jù)先于后續(xù)的讀取操作,避免了指令重排問題。
禁止指令重排原理是基于內(nèi)存屏障機(jī)制的。在Java中,為了提高程序的性能,編譯器和處理器可能會對指令進(jìn)行重排,從而優(yōu)化程序的執(zhí)行順序。但是,在多線程環(huán)境下,指令重排可能會導(dǎo)致程序出現(xiàn)意想不到的問題,因此需要禁止指令重排。Java中提供了volatile關(guān)鍵字和synchronized關(guān)鍵字來禁止指令重排。
當(dāng)一個變量被聲明為volatile時,在讀取和寫入變量時,會使用內(nèi)存屏障機(jī)制來確保變量的值的可見性和順序性。具體來說,讀取變量時,會插入Load Barrier,確保讀取的數(shù)據(jù)來自于主內(nèi)存而不是本地內(nèi)存;寫入變量時,會插入Store Barrier,確保寫入的數(shù)據(jù)同步到主內(nèi)存而不是本地內(nèi)存。這樣可以避免指令重排,保證變量的值的正確性和可見性。
當(dāng)一個代碼塊被synchronized關(guān)鍵字修飾時,會使用內(nèi)存屏障機(jī)制來確保代碼塊的原子性、可見性和順序性。具體來說,在進(jìn)入synchronized代碼塊前,會插入一個Lock Barrier,確保代碼塊的執(zhí)行順序和可見性;在退出synchronized代碼塊時,會插入一個Unlock Barrier,確保代碼塊的原子性和可見性。這樣可以避免指令重排,保證代碼塊的正確性和可見性。
具體來說,當(dāng)編譯器進(jìn)行指令重排時,會考慮到指令之間的依賴關(guān)系,如果不存在依賴關(guān)系,則可能會重排指令的執(zhí)行順序,以提高程序的執(zhí)行效率。然而,對于volatile變量的讀寫操作,編譯器必須保證這些操作的順序性,不能將其與其他指令重排,否則會導(dǎo)致程序出現(xiàn)意外的結(jié)果。因此,編譯器會在對volatile變量進(jìn)行讀寫操作時,插入Load Barrier和Store Barrier,以確保這些操作的順序性和可見性。這樣就可以禁止指令重排,保證volatile變量的值的正確性和可見性。
處理器也會進(jìn)行指令重排,同樣需要考慮到指令之間的依賴關(guān)系。當(dāng)處理器執(zhí)行一條Load指令時,如果沒有Load Barrier的限制,它可能會從本地內(nèi)存中讀取數(shù)據(jù),而不是從主內(nèi)存中讀取數(shù)據(jù)。同樣,當(dāng)處理器執(zhí)行一條Store指令時,如果沒有Store Barrier的限制,它可能會將數(shù)據(jù)寫入本地內(nèi)存,而不是主內(nèi)存。這些操作可能會導(dǎo)致程序出現(xiàn)意外的結(jié)果。因此,處理器也會在執(zhí)行Load和Store指令時,根據(jù)內(nèi)存屏障的類型,確保數(shù)據(jù)的順序性和可見性,避免指令重排的問題。
具體來說,當(dāng)編譯器進(jìn)行指令重排時,會考慮到指令之間的依賴關(guān)系,如果不存在依賴關(guān)系,則可能會重排指令的執(zhí)行順序,以提高程序的執(zhí)行效率。然而,對于volatile變量的讀寫操作,編譯器必須保證這些操作的順序性,不能將其與其他指令重排,否則會導(dǎo)致程序出現(xiàn)意外的結(jié)果。因此,編譯器會在對volatile變量進(jìn)行讀寫操作時,插入Load Barrier和Store Barrier,以確保這些操作的順序性和可見性。這樣就可以禁止指令重排,保證volatile變量的值的正確性和可見性。
處理器也會進(jìn)行指令重排,同樣需要考慮到指令之間的依賴關(guān)系。當(dāng)處理器執(zhí)行一條Load指令時,如果沒有Load Barrier的限制,它可能會從本地內(nèi)存中讀取數(shù)據(jù),而不是從主內(nèi)存中讀取數(shù)據(jù)。同樣,當(dāng)處理器執(zhí)行一條Store指令時,如果沒有Store Barrier的限制,它可能會將數(shù)據(jù)寫入本地內(nèi)存,而不是主內(nèi)存。這些操作可能會導(dǎo)致程序出現(xiàn)意外的結(jié)果。因此,處理器也會在執(zhí)行Load和Store指令時,根據(jù)內(nèi)存屏障的類型,確保數(shù)據(jù)的順序性和可見性,避免指令重排的問題。
JMM(Java Memory Model)
JMM描述
JMM 是Java內(nèi)存模型( Java Memory Model),簡稱JMM。它本身只是一個抽象的概念,并不真實存在,它描述的是一種規(guī)則或規(guī)范,是和多線程相關(guān)的一組規(guī)范。通過這組規(guī)范,定義了程序中對各個變量(包括實例字段,靜態(tài)字段和構(gòu)成數(shù)組對象的元素)的訪問方式。需要每個JVM 的實現(xiàn)都要遵守這樣的規(guī)范,有了JMM規(guī)范的保障,并發(fā)程序運行在不同的虛擬機(jī)上時,得到的程序結(jié)果才是安全可靠可信賴的。如果沒有JMM 內(nèi)存模型來規(guī)范,就可能會出現(xiàn),經(jīng)過不同 JVM 翻譯之后,運行的結(jié)果不相同也不正確的情況。
Java 是最早嘗試提供內(nèi)存模型的編程語言。由于早期內(nèi)存模型存在一些缺陷(比如非常容易削弱編譯器的優(yōu)化能力),從 Java 5 開始,Java 開始使用新的內(nèi)存模型 。
一般來說,編程語言也可以直接復(fù)用操作系統(tǒng)層面的內(nèi)存模型。不過,不同的操作系統(tǒng)內(nèi)存模型不同。如果直接復(fù)用操作系統(tǒng)層面的內(nèi)存模型,就可能會導(dǎo)致同樣一套代碼換了一個操作系統(tǒng)就無法執(zhí)行了。Java 語言是跨平臺的,它需要自己提供一套內(nèi)存模型以屏蔽系統(tǒng)差異。
這只是 JMM 存在的其中一個原因。實際上,對于 Java 來說,你可以把 JMM 看作是 Java 定義的并發(fā)編程相關(guān)的一組規(guī)范,除了抽象了線程和主內(nèi)存之間的關(guān)系之外,其還規(guī)定了從 Java 源代碼到 CPU 可執(zhí)行指令的這個轉(zhuǎn)化過程要遵守哪些和并發(fā)相關(guān)的原則和規(guī)范,其主要目的是為了簡化多線程編程,增強(qiáng)程序可移植性的。
synchronized 關(guān)鍵字通過 Java 內(nèi)存模型(Java Memory Model,JMM)來保證多線程之間的可見性和有序性。JMM 是一種規(guī)范,定義了 Java 程序在多線程環(huán)境下的內(nèi)存模型,規(guī)定了線程之間的共享變量如何在內(nèi)存中存儲、如何交互、以及在什么時候能看到修改等。
為什么要遵守這些并發(fā)相關(guān)的原則和規(guī)范呢?
這是因為并發(fā)編程下,像 CPU 多級緩存和指令重排這類設(shè)計可能會導(dǎo)致程序運行出現(xiàn)一些問題。就比如說我們上面提到的指令重排序就可能會讓多線程程序的執(zhí)行出現(xiàn)問題,為此,JMM 抽象了 happens-before 原則(后文會詳細(xì)介紹到)來解決這個指令重排序問題。
JMM 說白了就是定義了一些規(guī)范來解決這些問題,開發(fā)者可以利用這些規(guī)范更方便地開發(fā)多線程程序。對于 Java 開發(fā)者說,你不需要了解底層原理,直接使用并發(fā)相關(guān)的一些關(guān)鍵字和類(比如 volatile、synchronized、各種 Lock)即可開發(fā)出并發(fā)安全的程序。
JMM 是如何抽象線程和主內(nèi)存之間的關(guān)系?
Java 內(nèi)存模型(JMM) 抽象了線程和主內(nèi)存之間的關(guān)系,就比如說線程之間的共享變量必須存儲在主內(nèi)存中。
在 Java 中,線程之間的通信和數(shù)據(jù)共享是通過內(nèi)存來實現(xiàn)的。Java 內(nèi)存模型(Java Memory Model,JMM)是一種規(guī)范,定義了多線程程序中,線程之間如何與主內(nèi)存進(jìn)行交互以及如何對共享變量進(jìn)行訪問和修改。
在JMM中,每個線程都有自己的工作內(nèi)存(也稱為本地內(nèi)存),它存儲著線程私有的數(shù)據(jù)副本,包括棧幀、程序計數(shù)器等。而主內(nèi)存則是所有線程共享的內(nèi)存區(qū)域,它存儲著所有的共享變量。
當(dāng)線程訪問共享變量時,它首先會把共享變量從主內(nèi)存中讀取到自己的工作內(nèi)存中進(jìn)行操作,包括讀取、修改和寫入。然后,線程對變量的操作完成后,必須將結(jié)果刷新到主內(nèi)存中,以便其他線程可以看到最新的值。這個過程可以看作是線程和主內(nèi)存之間的數(shù)據(jù)同步。
JMM規(guī)定了多線程在執(zhí)行時的一些重要規(guī)則和原則,如原子性、可見性和有序性:
- 原子性:JMM保證了對共享變量的讀取和寫入可以被視為原子操作,即線程要么完全看到共享變量的修改結(jié)果,要么不看到。
- 可見性:當(dāng)一個線程對共享變量進(jìn)行修改后,在刷新到主內(nèi)存之前,其他線程不一定能立即看到這個修改。為了確??梢娦裕枰ㄟ^volatile關(guān)鍵字或者使用synchronized或Lock等同步機(jī)制來進(jìn)行同步操作。
- 有序性:JMM保證了線程內(nèi)的操作按照程序的順序執(zhí)行,但不保證不同線程的操作順序。為了保證有序性,需要使用volatile關(guān)鍵字、synchronized或Lock等同步機(jī)制或者使用顯式的內(nèi)存屏障。
總之,Java內(nèi)存模型提供了一套規(guī)則和原則,保證了多線程程序中線程之間的通信和數(shù)據(jù)共享的正確性和一致性。通過有效使用JMM提供的同步機(jī)制,我們可以確保共享變量在多線程環(huán)境下的正確訪問和修改。
在 JDK1.2 之前,Java 的內(nèi)存模型實現(xiàn)總是從 主存 (即共享內(nèi)存)讀取變量,是不需要進(jìn)行特別的注意的。而在當(dāng)前的 Java 內(nèi)存模型下,線程可以把變量保存 本地內(nèi)存 (比如機(jī)器的寄存器)中,而不是直接在主存中進(jìn)行讀寫。這就可能造成一個線程在主存中修改了一個變量的值,而另外一個線程還繼續(xù)使用它在寄存器中的變量值的拷貝,造成數(shù)據(jù)的不一致。
這和我們上面講到的 CPU 緩存模型非常相似。
什么是主內(nèi)存?什么是本地內(nèi)存?
-
主內(nèi)存 : 所有線程創(chuàng)建的實例對象都存放在主內(nèi)存中,不管該實例對象是成員變量還是方法中的本地變量(也稱局部變量)
-
本地內(nèi)存 : 每個線程都有一個私有的本地內(nèi)存來存儲共享變量的副本,并且,每個線程只能訪問自己的本地內(nèi)存,無法訪問其他線程的本地內(nèi)存。本地內(nèi)存是 JMM 抽象出來的一個概念,存儲了主內(nèi)存中的共享變量副本。
從上圖來看,線程 A 與線程 B之間如果要進(jìn)行通信的話,必須要經(jīng)歷下面 2 個步驟:
- 線程 A 把本地內(nèi)存中修改過的共享變量副本的值同步到主內(nèi)存中去。
- 線程 B到主存中讀取對應(yīng)的共享變量的值。
也就是說,JMM 為共享變量提供了可見性的保障。
不過,多線程下,對主內(nèi)存中的一個共享變量進(jìn)行操作有可能誘發(fā)線程安全問題。舉個例子:
- 線程 A 和線程 B 分別對同一個共享變量進(jìn)行操作,一個執(zhí)行修改,一個執(zhí)行讀取。
- 線程 B 讀取到的是線程 A 修改之前的值還是修改后的值并不確定,都有可能,因為線程 A 和線程 B 都是先將共享變量從主內(nèi)存拷貝到對應(yīng)線程的本地內(nèi)存中。
關(guān)于主內(nèi)存與工作內(nèi)存之間的具體交互協(xié)議,即一個變量如何從主內(nèi)存拷貝到工作內(nèi)存,如何從工作內(nèi)存同步到主內(nèi)存之間的實現(xiàn)細(xì)節(jié),Java 內(nèi)存模型定義來以下八種同步操作(了解即可):
- lock(鎖定): 作用于主內(nèi)存中的變量,將他標(biāo)記為一個線程獨享變量。
- unlock(解鎖): 作用于主內(nèi)存中的變量,解除變量的鎖定狀態(tài),被解除鎖定狀態(tài)的變量才能被其他線程鎖定。
- read(讀?。鹤饔糜谥鲀?nèi)存的變量,它把一個變量的值從主內(nèi)存?zhèn)鬏數(shù)骄€程的工作內(nèi)存中,以便隨后的 load 動作使用。
- load(載入):把 read 操作從主內(nèi)存中得到的變量值放入工作內(nèi)存的變量的副本中。
- use(使用):把工作內(nèi)存中的一個變量的值傳給執(zhí)行引擎,每當(dāng)虛擬機(jī)遇到一個使用到變量的指令時都會使用該指令。
- assign(賦值):作用于工作內(nèi)存的變量,它把一個從執(zhí)行引擎接收到的值賦給工作內(nèi)存的變量,每當(dāng)虛擬機(jī)遇到一個給變量賦值的字節(jié)碼指令時執(zhí)行這個操作。
- store(存儲):作用于工作內(nèi)存的變量,它把工作內(nèi)存中一個變量的值傳送到主內(nèi)存中,以便隨后的 write 操作使用。
- write(寫入):作用于主內(nèi)存的變量,它把 store 操作從工作內(nèi)存中得到的變量的值放入主內(nèi)存的變量中。
除了這 8 種同步操作之外,還規(guī)定了下面這些同步規(guī)則來保證這些同步操作的正確執(zhí)行(了解即可):
- 不允許一個線程無原因地(沒有發(fā)生過任何 assign 操作)把數(shù)據(jù)從線程的工作內(nèi)存同步回主內(nèi)存中。
- 一個新的變量只能在主內(nèi)存中 “誕生”,不允許在工作內(nèi)存中直接使用一個未被初始化(load 或 assign)的變量,換句話說就是對一個變量- 實施 use 和 store 操作之前,必須先執(zhí)行過了 assign 和 load 操作。
- 一個變量在同一個時刻只允許一條線程對其進(jìn)行 lock 操作,但 lock 操作可以被同一條線程重復(fù)執(zhí)行多次,多次執(zhí)行 lock 后,只有執(zhí)行相同次數(shù)的 unlock 操作,變量才會被解鎖。
- 如果對一個變量執(zhí)行 lock 操作,將會清空工作內(nèi)存中此變量的值,在執(zhí)行引擎使用這個變量前,需要重新執(zhí)行 load 或 assign 操作初始化變量的值。
- 如果一個變量事先沒有被 lock 操作鎖定,則不允許對它執(zhí)行 unlock 操作,也不允許去 unlock 一個被其他線程鎖定住的變量。
- …
Java 內(nèi)存區(qū)域和 JMM 有何區(qū)別?
Java 內(nèi)存區(qū)域和內(nèi)存模型是完全不一樣的兩個東西 :
-
JVM 內(nèi)存結(jié)構(gòu)和 Java 虛擬機(jī)的運行時區(qū)域相關(guān),定義了 JVM 在運行時如何分區(qū)存儲程序數(shù)據(jù),就比如說堆主要用于存放對象實例。
-
Java 內(nèi)存模型和 Java 的并發(fā)編程相關(guān),抽象了線程和主內(nèi)存之間的關(guān)系就比如說線程之間的共享變量必須存儲在主內(nèi)存中,規(guī)定了從 Java 源代碼到 CPU 可執(zhí)行指令的這個轉(zhuǎn)化過程要遵守哪些和并發(fā)相關(guān)的原則和規(guī)范,其主要目的是為了簡化多線程編程,增強(qiáng)程序可移植性的。
happens-before 原則
happens-before 原則定義
-
如果一個操作 happens-before 另一個操作,那么第一個操作的執(zhí)行結(jié)果將對第二個操作可見,而且第一個操作的執(zhí)行順序排在第二個操作之前。
-
兩個操作之間存在 happens-before 關(guān)系,并不意味著 Java 平臺的具體實現(xiàn)必須要按照 happens-before 關(guān)系指定的順序來執(zhí)行。如果重排序之后的執(zhí)行結(jié)果,與按happens-before 關(guān)系來執(zhí)行的結(jié)果一致,那么這種重排序并不非法(也就是說,JMM 允許這種重排序)。
為什么引入happens-before 原則?
happens-before 原則的誕生是為了程序員和編譯器、處理器之間的平衡。程序員追求的是易于理解和編程的強(qiáng)內(nèi)存模型,遵守既定規(guī)則編碼即可。編譯器和處理器追求的是較少約束的弱內(nèi)存模型,讓它們盡己所能地去優(yōu)化性能,讓性能最大化。happens-before 原則的設(shè)計思想其實非常簡單:
- 為了對編譯器和處理器的約束盡可能少,只要不改變程序的執(zhí)行結(jié)果(單線程程序和正確執(zhí)行的多線程程序),編譯器和處理器怎么進(jìn)行重排序優(yōu)化都行。
- 對于會改變程序執(zhí)行結(jié)果的重排序,JMM 要求編譯器和處理器必須禁止這種重排序。
代碼分析
int stepA = 6;
int stepB = 6;
int stepC = stepA + stepB;
stepA happens-before stepB
stepA happens-before stepC
stepB happens-before stepC
雖然 stepA happens-before stepB,但對 stepA 和 stepB 進(jìn)行重排序不會影響代碼的執(zhí)行結(jié)果,所以 JMM 是允許編譯器和處理器執(zhí)行這種重排序的。但 stepA 和 stepB 必須是在 stepC 執(zhí)行之前,也就是說 stepA,stepB happens-before stepC 。
happens-before 原則表達(dá)的意義其實并不是一個操作發(fā)生在另外一個操作的前面,雖然這從程序員的角度上來說也并無大礙。更準(zhǔn)確地來說,它更想表達(dá)的意義是前一個操作的結(jié)果對于后一個操作是可見的,無論這兩個操作是否在同一個線程里。
舉個例子:操作 stepA happens-before 操作 stepB,即使操作 stepA 和操作 stepB 不在同一個線程內(nèi),JMM 也會保證操作 stepA 的結(jié)果對操作 stepB 是可見的。
happens-before 常見規(guī)則
定義
- 程序次序規(guī)則(Program Order Rule):在一個線程內(nèi),按照控制流順序,書寫在前面的操作先行發(fā)生于書寫在后面的操作。
- 管程鎖定規(guī)則(Monitor Lock Rule):一個unlock操作先行發(fā)生于后面對同一個鎖的lock操作。
- volatile變量規(guī)則(Volatile Variable Rule):對一個volatile變量的寫操作先行發(fā)生于后面對這個變量的讀操作。
- 線程啟動規(guī)則(Thread Start Rule):Thread對象start()方法先行發(fā)生于此線程的每一個動作。
- 線程終止規(guī)則(Thread Termination Rule):線程中的所有操作都先行發(fā)生于對此線程的終止檢測,我們可以通過Thread.join()方法和Thread.isAlive()的返回值等手段檢測線程是否已經(jīng)終止執(zhí)行。
- 線程中斷規(guī)則(Thread Interruption Rule):對線程interrupt()方法的調(diào)用先行發(fā)生于被中斷線程的代碼檢測到中斷事件的發(fā)生,可以通過- Thread.interrupted()方法檢測到是否有中斷發(fā)生。
- 對象終結(jié)規(guī)則(Finalizer Rule) :一個對象的初始化完成(構(gòu)造函數(shù)結(jié)束)先行發(fā)生于它的finalize()方法的開始。
- 傳遞性(Transitivity):如果操作A先行發(fā)生于操作B,操作B先行發(fā)生于操作C,那就可以得出操作A先行發(fā)生于操作C的結(jié)論。
通俗理解
- 程序次序規(guī)則:在一個線程內(nèi),按照控制流順序,如果操作A先行發(fā)生于操作B,那么操作A所產(chǎn)生的影響對于操作B是可見的。
- 管程鎖定規(guī)則:對于同一個鎖,如果一個unlock操作先行發(fā)生于一個lock操作,那么該unlock操作所產(chǎn)生的影響對于該lock操作是可見的。
- volatile變量規(guī)則:對于同一個volatile變量,如果對于這個變量的寫操作先行發(fā)生于這個變量的讀操作,那么對于這個變量的寫操作所產(chǎn)的影響對于這個變量的讀操作是可見的。
- 線程啟動規(guī)則:對于同一個Thread對象,該Thread對象的start()方法先行發(fā)生于此線程的每一個動作,也就是說對線程start()方法調(diào)用所產(chǎn)生的影響對于該該線程的每一個動作都是可見的。
- 線程終止規(guī)則:對于一個線程,線程中發(fā)生的所有操作先行發(fā)生于對此線程的終止檢測,也就是說線程中的所有操作所產(chǎn)生的影響對于調(diào)用線程Thread.join()方法或者Thread.isAlive()方法都是可見的。
- 線程中斷規(guī)則:對于同一個線程,對線程interrupt()方法的調(diào)用先行發(fā)生于該線程檢測到中斷事件的發(fā)生,也就是說線程interrupt()方法調(diào)用所產(chǎn)生的影響對于該線程檢測到中斷事件是可見的。
- 對象終結(jié)規(guī)則:對于同一個對象,它的構(gòu)造方法執(zhí)行結(jié)束先行發(fā)生于它的finalize()方法的開始,也就是說一個對象的構(gòu)造方法結(jié)束所產(chǎn)生的影響,對于它的finalize()方法開始執(zhí)行是可見的。
- 傳遞性:如果操作A先行發(fā)生于操作B,操作B先行發(fā)生于操作C,則操作A先行發(fā)生于操作C,也就說操作A所產(chǎn)生的所有影響對于操作C是可見的。
happens-before 和 JMM 什么關(guān)系?
文章來源:http://www.zghlxwxcb.cn/news/detail-525608.html
如果想了解更多,可以參考這篇文章——JMM面試題文章來源地址http://www.zghlxwxcb.cn/news/detail-525608.html
到了這里,關(guān)于JMM(Java 內(nèi)存模型)詳解的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!