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