JVM——CPU缓存架构与Java 内存模型
導(dǎo)航
- 一、CPU緩存架構(gòu)與一致性協(xié)議
- 1.1 CPU緩存架構(gòu)
- 1.2 緩存行與偽共享問(wèn)題
- 1.3 MESI 緩存一致性協(xié)議
- 1.4 偽共享的解決辦法
- 二、JMM Java 內(nèi)存模型
- 2.1 JMM 簡(jiǎn)介
- 2.2 原子性、可見(jiàn)性、有序性
- 2.3 八大內(nèi)存交互操作
- 2.4 happens-before 原則
一、CPU緩存架構(gòu)與一致性協(xié)議
1.1 CPU緩存架構(gòu)
現(xiàn)代 CPU 的發(fā)展非常快,內(nèi)存的速度已經(jīng)完全跟不上。如果將CPU完成一個(gè)基本操作所用的時(shí)間定義為時(shí)鐘周期,那么 CPU 的指令處理速度要比內(nèi)存的加載速度快100 倍左右。
為了解決這個(gè)性能上的鴻溝,現(xiàn)代 CPU 架構(gòu)往往采用如下圖所示的緩存架構(gòu):
在多核CPU和主存(Main Memory)之間引入三級(jí)高速緩存——L1、L2、L3。
越靠近CPU的緩存,成本造價(jià)越高、性能越強(qiáng)、存儲(chǔ)空間越小,其中 L3 緩存是多核共享,而L1、L2 是核內(nèi)私有。
1.2 緩存行與偽共享問(wèn)題
緩存行 Cache Line 是高速緩存一次讀取內(nèi)存數(shù)據(jù)的最小單位。目前最常見(jiàn)的 Intel cpu 的緩存行大小是 64 Bytes。
連續(xù)的數(shù)據(jù)很有可能由于數(shù)據(jù)跨度恰好在一個(gè)緩存行內(nèi),就很有可能會(huì)被CPU加載到 L1、L2、L3 高速緩存中。
由于高速緩存的存在,引出了緩存一致性協(xié)議,它可以保證 L1、L2、L3 中的數(shù)據(jù)在多核CPU和并發(fā)場(chǎng)景下不會(huì)出現(xiàn)線程不一致問(wèn)題。早期的解決方案并不是各種緩存一致性方案,而是采用總線鎖的方式。
總線鎖,顧名思義就是將高速緩存與主內(nèi)存之間的總線上鎖,只允許一個(gè)線程去獲取并操作上鎖數(shù)據(jù),處理完成后釋放鎖,并將數(shù)據(jù)從緩存中刷回主存。這種方式雖然安全,但也犧牲了很大的性能。于是,人們又引入了諸如 MESI 的緩存一致性協(xié)議。簡(jiǎn)單的說(shuō),就是給緩存數(shù)據(jù)做標(biāo)記,如果CPU發(fā)現(xiàn)緩存的數(shù)據(jù)失效了,就必須從主存中重新加載最新的數(shù)據(jù)。
那什么又是偽共享呢?
偽共享是緩存行加載數(shù)據(jù)時(shí)必然會(huì)存在的性能損耗問(wèn)題。當(dāng)對(duì)象中包含線程局部變量,且尺寸大小小于 64 字節(jié),就有可能發(fā)生偽共享問(wèn)題。
再具體點(diǎn),內(nèi)存地址連續(xù)的不同變量被加載到了同一個(gè)緩存行中,而同一個(gè)緩存行中的多個(gè)變量,又不一定是CPU所需的,這種情況就是偽共享。
由于偽共享的存在,CPU核心中的高速緩存加載了本不需要數(shù)據(jù),又在“緩存一致性協(xié)議”的要求下,不得不在這些無(wú)用數(shù)據(jù)失效后重新加載緩存,導(dǎo)致緩存失效,或造成性能損耗:
如上圖所示,x、y 兩個(gè)變量由 Main Memory 加載到 core 1 和 core 2兩個(gè)核心的 L1、L2 緩存中。如果線程A、B 跑在兩個(gè)核心上,且線程 A 修改 core 1 中的 x,由于緩存一致性協(xié)議, core 2 中的 x 變量將會(huì)失效,它必須從主內(nèi)存中重新加載,這樣頻繁的加載、訪問(wèn)主內(nèi)存, core 2 中的 L1、L2 緩存幾乎等于失效。
1.3 MESI 緩存一致性協(xié)議
MESI是緩存鎖的實(shí)現(xiàn)方式之一,注意這是 CPU 緩存一致性,并非通常說(shuō)的應(yīng)用緩存。有些無(wú)法被緩存的數(shù)據(jù)或跨多個(gè)緩存行的數(shù)據(jù)依然必須使用總線鎖。
MESI 的核心思想是為每個(gè) Cache Line 標(biāo)記 4 種狀態(tài):
1.4 偽共享的解決辦法
Java 7 之前可以采用字節(jié)填充的方式,例如針對(duì)不同操作系統(tǒng)和對(duì)象頭的大小,補(bǔ)齊多個(gè) long 類(lèi)型的空數(shù)據(jù)。
但這種方式在 Java 7 的某個(gè)版本中會(huì)出現(xiàn) 填充失效問(wèn)題,原因是該版本的虛擬機(jī)優(yōu)化了未使用 field 的排布。
在 Java 8 加入 @Contended 注解會(huì)幫助增加128 字節(jié)的 padding,并且需要開(kāi)啟 -XX:-RestrictContended 選項(xiàng)才能生效。
雖然解決了偽共享的問(wèn)題,但是這種填充的方式也浪費(fèi)了緩存資源,而且緩存又小又貴,時(shí)間和空間的取舍要酌情考慮。
二、JMM Java 內(nèi)存模型
2.1 JMM 簡(jiǎn)介
JMM 全稱是 “Java Memory Model”,Java 內(nèi)存模型。
因?yàn)樵诓煌挠布a(chǎn)商和操作系統(tǒng)下,內(nèi)存的訪問(wèn)方式各有所差異,這樣就會(huì)造成相同的代碼出現(xiàn)不一樣的問(wèn)題,而 JMM 屏蔽掉了各種操作系統(tǒng)的內(nèi)存訪問(wèn)差異,以實(shí)現(xiàn)“Write Once,Run Anywhere”的目標(biāo)。
JMM 中規(guī)定所有的變量都存儲(chǔ)在主內(nèi)存 (Main Mem)中,包括實(shí)例變量、靜態(tài)變量,但是不包括局部變量和方法參數(shù)。每條線程都有自己的工作內(nèi)存(Work Mem),線程私有。工作內(nèi)存中保存的是線程的變量從主內(nèi)存中的拷貝副本。
這種結(jié)構(gòu)的基本工作方式是:線程對(duì)變量的讀和寫(xiě)都必須在工作內(nèi)存中進(jìn)行,而線程之間變量值的傳遞均需要通過(guò)主內(nèi)存來(lái)完成。如下圖所示,注意與Java 內(nèi)存結(jié)構(gòu)(堆棧等)等概念區(qū)分開(kāi)。
2.2 原子性、可見(jiàn)性、有序性
整個(gè) JMM 實(shí)際上是圍繞著三個(gè)并發(fā)特征建立起來(lái)的——原子性、可見(jiàn)性、有序性。
2.3 八大內(nèi)存交互操作
說(shuō)是 8 種內(nèi)存交互操作:
- lock 和 unlock:鎖定與解鎖。作用于主內(nèi)存的變量,將其設(shè)置為線程獨(dú)占或解除。
- Read 和 Write:發(fā)生在主內(nèi)存和工作內(nèi)存之間,將變量傳輸?shù)焦ぷ鲀?nèi)存,將從工作內(nèi)存得到的值放入主內(nèi)存中。
- Load 和 Store:作用于工作內(nèi)存的變量,將工作內(nèi)存中的變量放入副本中,將工作內(nèi)存中的變量傳輸?shù)街鲀?nèi)存中。
- Use 和 Assign:將工作內(nèi)存中的變量傳輸?shù)綀?zhí)行引擎,將一個(gè)從執(zhí)行引擎中接收到的值賦值給工作內(nèi)存的副本。
使用規(guī)則:
- 不允許 read、load、store、write 操作之一單獨(dú)出現(xiàn),即 read 操作后必須 load,store后必須 write。
- 不允許線程丟棄它最近的 assign 操作,即工作內(nèi)存中的變量修改之后,必須告知主存。
- 不允許線程將沒(méi)有 assign 的數(shù)據(jù)從工作內(nèi)存同步到主存。
- lock 和 unlock 必須成對(duì)出現(xiàn)。
- 新的變量必須由主存中誕生。
- 如果對(duì)一個(gè)變量進(jìn)行 lock,會(huì)清空所有工作內(nèi)存中此變量的值。在執(zhí)行引擎使用這個(gè)變量前,必須重新 load 或 assign 。
- unlock 之前,必須將此變量同步回主內(nèi)存。
2.4 happens-before 原則
happens-before是JMM提供的有序性保證。在理解它的概念之前,需要了解一下弱內(nèi)存模型和強(qiáng)內(nèi)存模型。
弱內(nèi)存模型——weak memory model
弱內(nèi)存模型中,loadload、loadstore、storestore、storeload 四種內(nèi)存重排序都有可能發(fā)生。只要不改變單線程行為,弱內(nèi)存模型可隨意對(duì)代碼進(jìn)行重排序,這是一種近乎不存在任何保證的模型。
強(qiáng)內(nèi)存模型
Java的內(nèi)存模型屬于一種叫做 “順序一致性” 的強(qiáng)內(nèi)存模型。在順序一致性模型中,不再存在重排序,Java的JMM就屬于這種強(qiáng)內(nèi)存模型。
雖然 JMM 屬于最高級(jí)別的強(qiáng)內(nèi)存模型——順序一致性內(nèi)存模型,但并不是說(shuō)Java 不允許重排序,而是在某些情況下,可以支持嚴(yán)格限制重排序的目的,如volatile。而弱內(nèi)存模型是無(wú)法滿足這種保證的。
happens-before 是 JMM 提供的一系列關(guān)于重排序問(wèn)題的規(guī)則。可以概括為:
If one action happens-before another, then the first is visible to and ordered before the second.
如果一個(gè)動(dòng)作 happens-before 另一個(gè)動(dòng)作,那么前者的執(zhí)行會(huì)排在后者的前面,并且(其結(jié)果)對(duì)后者可見(jiàn)。
《java language specification 8》
可以分為以下兩種策略:
這兩個(gè)策略概括起來(lái),可以這樣理解——只要不改變程序的執(zhí)行結(jié)果,編譯器和處理器想怎么優(yōu)化就怎么優(yōu)化。
例如,如果編譯器經(jīng)過(guò)細(xì)致的分析,認(rèn)定一個(gè)鎖只能被一個(gè)線程訪問(wèn)到,那么這個(gè)鎖可以被消除。
又例如,編譯器認(rèn)定一個(gè) volatile 變量只會(huì)被單個(gè)線程訪問(wèn)到,那么它可以把這個(gè) volatile 變量當(dāng)做一個(gè)普通變量對(duì)待。
happens-before 的具體規(guī)則如下:
根據(jù)這些規(guī)則,以下這些場(chǎng)景都是 happens-before 的情況:
總之,happens-before 定義了兩個(gè)動(dòng)作發(fā)生資源競(jìng)爭(zhēng)時(shí)的時(shí)間順序,是JMM 描述特定動(dòng)作前后執(zhí)行順序的一種抽象關(guān)系。它原則上不允許重排序,但在一些不影響程序執(zhí)行結(jié)果的情況下,亂序執(zhí)行也是可以的。
總結(jié)
以上是生活随笔為你收集整理的JVM——CPU缓存架构与Java 内存模型的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: Mybatis-puls打印sql语句
- 下一篇: 从零开始学java 框架_从零开始学 J