
點贊關注,不再迷路,你的支持對我意義重大!
?? Hi,我是丑丑。本文 「計算機組成原理」| 導讀 —— 已收錄,這里有 Android 進階成長路線筆記 & 博客,歡迎跟著彭丑丑一起成長。(聯(lián)系方式在 GitHub)
前言
- CPU 緩存是計算機組成原理中比較基礎,同時也是比較常用的知識,面試中也可能會有一定延伸;
- 在這篇文章里,我將總結(jié)CPU 緩存 & 緩存一致性 & 偽共享 等問題。如果能幫上忙,請務必點贊加關注,這真的對我非常重要。
目錄

1. CPU 三級緩存
- 背景: CPU 處理器的運算速度與內(nèi)存存取速度、磁盤 I/O 速度不匹配(相差了幾個數(shù)量級);
- 目的: 提高 CPU 吞吐量;
- 方案: 增加一個緩存層來協(xié)調(diào)兩者的速度差,即:在 CPU 和內(nèi)存中間增加一層 「高速緩存」,緩存的存取速度盡可能接近。
數(shù)據(jù)加載的流程如下:
- 1、將程序和數(shù)據(jù)從磁盤加載到內(nèi)存中;
- 2、將程序和數(shù)據(jù)從內(nèi)存加載到緩存中(三級緩存,數(shù)據(jù)加載順序:L3->L2->L1);
- 3、CPU將緩存中的數(shù)據(jù)加載到寄存器中(L0),并進行運算;
- 4、CPU將數(shù)據(jù)同步回緩存,并在一定的時間周期之后同步回內(nèi)存。
經(jīng)驗表明,CPU 往往需要重復讀取同一個數(shù)據(jù)塊,而緩存容量的增大,可以大幅度提升 CPU 內(nèi)部讀取數(shù)據(jù)的命中率,以此提升 CPU 吞吐量。但是出于 CPU 芯片體積和價格因素來考慮,緩存都不會很大?,F(xiàn)代 CPU 芯片使用的是三級的高速緩存:

—— 圖片引用自網(wǎng)絡
最開始是寄存器(也稱為 L0 緩存),接下來是 L1,L2,L3 三級緩存,最后是內(nèi)存,本地磁盤,遠程存儲。從上到下空間越大,速度越慢,但是成本越低。需要注意的是:在現(xiàn)代 CPU 里,L0、L1、L2、L3 都集成在一顆 CPU 內(nèi)部,其中 L0、L1、L2 是每個處理核心獨立的,而 L3 是一顆 CPU 的多個處理器共用的。

—— 圖片引用自網(wǎng)絡
2. 緩存一致性問題
現(xiàn)代 CPU 通常有多個核心,每個核心也都有自己獨立的緩存(L1、L2 緩存),當多個核心同時操作同一個數(shù)據(jù)時,如果核心 2 在核心 1 還未將更新的數(shù)據(jù)同步回內(nèi)存之前讀取了數(shù)據(jù),就出現(xiàn)了緩存不一致問題。
舉個例子,假設線程 A 和線程 B 同時對一個變量執(zhí)行 i++,就可能存在緩存不一致問題:
- 1、核心 A 和核心 B 從內(nèi)存中加載了 i 的值,并且緩存到各自的高速緩存中,此時兩個副本都為0;
- 2、核心 A 進行加一操作,副本值變成了 1,最后回寫到主存中,主存中的值為 1;
- 3、核心 B 進行加一操作,副本值變成了 1,最后回寫到主存中,主存中的值為 1;
- 4、最終主存的值為 1,而不是期望的 2。
為了解決緩存不一致性問題,通常來說有兩種解決方法:
- 1、鎖總線
早期的 CPU 是通過在鎖總線來解決緩存不一致的問題。鎖總線是對整個內(nèi)存加鎖,在鎖總線期間,其他處理器無法訪問內(nèi)存,可想而知會嚴重降低 CPU 性能。
- 2、緩存一致性協(xié)議(MESI)
緩存一致性協(xié)議提供了一種高效的內(nèi)存數(shù)據(jù)管理方案。「鎖內(nèi)存方案」相當于保證了整塊內(nèi)存的一致性,而「緩存一致性協(xié)議方案」本質(zhì)上相當與一致性保護范圍,從整塊內(nèi)存縮小為單個緩存行(緩存行是緩存的基本單元)。
簡單來說:當 CPU 核心準備寫數(shù)據(jù)時,如果發(fā)現(xiàn)操作的變量是共享變量(即在其他核心中也存在該變量的副本),就會通知其他核心該變量「緩存行」無效,需要重新從內(nèi)存讀取。
具體來說,MESI 協(xié)議會將緩存數(shù)據(jù)定義為四種狀態(tài):
| 狀態(tài) | 描述 |
|---|---|
| E(Exclusive) | 獨享 / 互斥 |
| S(Shared) | 共享 |
| M(Modify) | 修改 |
| I(Invalid) | 無效 |
詳細工作原理:
- 1、核心 A 從內(nèi)存中加載變量 i,并將緩存行設置為 E(獨享),隨后通過總線嗅探檢查內(nèi)存中對變量 i 的操作;
- 2、核心 B 從內(nèi)存中加載變量 i,總線嗅探機制會將核心 A 與核心 B 的緩存行設置為 S(共享);
- 3、核心 A 對變量 i 進行修改,緩存行設置為 M(修改),而核心 B 被通知修改緩存行為 I(無效)。如果存在高并發(fā),則交給總線裁決;
- 4、核心 A 將修改后數(shù)據(jù)同步回內(nèi)存,并將變量設置為 E(獨享);
- 5、核心 B 重新刷新緩存行,并將緩存行核心 A 和核心 B 的緩存行設置為 S(共享)。
易混淆: MESI 協(xié)議是 CPU 的協(xié)議,JMM 也有自己的協(xié)議來保證緩存一致性。
3. 偽共享(False Sharing)
在 CPU 緩存中,緩存管理的基本單位并不是「字節(jié)」,而是「緩存行(Cache Line)」。緩存行的大小取決于 CPU,一般是 64 字節(jié)。
緩存行的設計源于:“CPU 讀取一個數(shù)據(jù)之后,往往還需要重復讀取附近的數(shù)據(jù)”。使用緩存行一次將一小塊數(shù)據(jù)加載進高速緩存,有助于提高運算效率。

—— 圖片引用自網(wǎng)絡
當然,緩存也不是完美的,也存在副作用 —— 偽共享。偽共享是指多個線程同時讀寫同一個緩存行中的變量,而導致緩存行失效的問題。盡管兩個線程分別訪問的是不同的數(shù)據(jù),但由于它們存在同一個緩存行中,只要任何一方修改都會使得緩存失效,降低了運算效率。
解決偽共享的方法是 「字節(jié)填充」,即通過在兩個變量中間的內(nèi)容填充額外的字節(jié),使得兩個變量存放在到不同緩存行中,從而規(guī)避偽共享問題。
在使用字節(jié)填充時,需要先考慮哪些變量是獨立變化的,哪些變量是協(xié)同變化的。協(xié)同變化的變量放在一組,而無關的變量分到不同組。這樣當修改變量時,不會導致無關變量的緩存行無效。
在 Java 中,Java 8 前后的處理方式不同:
- Java 8 之前:通過填充 long 變量來分組
public class DataPadding{
long a1,a2,a3,a4,a5,a6,a7; 防止與前一個對象產(chǎn)生偽共享
int value1;
long value2;
long b1,b2,b3,b4,b5,b6,b7; 防止兩組變量偽共享;
boolean flag;
long d1,d2,d3,d4,d5,d6,d7; 防止與下一個對象產(chǎn)生偽共享
}
- Java 8:通過 @sun.misc.Contended 分組
@Contended 注解是 JDK 1.8 新增的注解,用于將變量劃分到不同的緩存行。
例如:
Java 8 Thread.java
/** The current seed for a ThreadLocalRandom */
@sun.misc.Contended("tlr")
long threadLocalRandomSeed;
/** Probe hash value; nonzero if threadLocalRandomSeed initialized */
@sun.misc.Contended("tlr")
int threadLocalRandomProbe;
/** Secondary seed isolated from public ThreadLocalRandom sequence */
@sun.misc.Contended("tlr")
int threadLocalRandomSecondarySeed;
Java 8 ConcurrentHashMap.java
@sun.misc.Contended static final class CounterCell {
volatile long value;
CounterCell(long x) { value = x; }
}
提示: 需要 JVM 開啟字節(jié)填充功能 -XX:-RestrictContended
4. 總結(jié)
由于 CPU 處理器的運算速度與內(nèi)存存取速度、磁盤 I/O 速度不匹配,所以 CPU 中會增加一層緩存來協(xié)調(diào)兩者的速度差。CPU 緩存是一個三級結(jié)構(gòu),其中 L0、L1、L2 是每個處理核心獨立的,而 L3 是一顆 CPU 的多個處理器共用的;
由于 CPU 每個核心也都有自己獨立的緩存(L1、L2 緩存),當多個核心同時操作同一個數(shù)據(jù)時,如果核心 2 在核心 1 還未將更新的數(shù)據(jù)同步回內(nèi)存之前讀取了數(shù)據(jù),就出現(xiàn)了緩存不一致問題。解決方案有「鎖總線」&「緩存一致性協(xié)議」;
由于 “CPU 讀取一個數(shù)據(jù)之后,往往還需要重復讀取附近的數(shù)據(jù)”,所以 CPU 設計了緩存行(Cache Line)作為基本單位。當然,緩存頁存在副作用 —— 偽共享,即:當多個線程同時讀寫同一個緩存行中的變量,而導致緩存行失效的問題。解決方案是「字節(jié)填充」,使得兩個變量存放在到不同緩存行中。Java 8 之前采用填充 long 變量,而 Java 8 之后采用 @sun.misc.Contended 注解。
創(chuàng)作不易,你的「三連」是丑丑最大的動力,我們下次見!
