CPU緩存及偽共享
什么是CPU緩存
在計算機系統(tǒng)中,CPU高速緩存是用于減少處理器訪問內存所需平均時間的部件。在金字塔式存儲體系中它位于自頂向下的第二層,僅次于CPU寄存器。其容量遠小于內存,但速度卻可以接近處理器的頻率。
當處理器發(fā)出內存訪問請求時,會先查看緩存內是否有請求數據。如果存在(命中),則不經訪問內存直接返回該數據;如果不存在(失效),則要先把內存中的相應數據載入緩存,再將其返回處理器。

為什么需要CPU緩存
隨著工藝的提升,最近幾十年 CPU 的頻率不斷提升,而受制于制造工藝和成本限制,目前計算機的內存在訪問速度上沒有質的突破。因此,CPU 的處理速度和內存的訪問速度差距越來越大,甚至可以達到上萬倍。這種情況下傳統(tǒng)的 CPU 直連內存的方式顯然就會因為內存訪問的等待,導致計算資源大量閑置,降低 CPU 整體吞吐量。同時又由于內存數據訪問的熱點集中性,在 CPU 和內存之間用較為快速而成本較高(相對于內存)的介質做一層緩存,就顯得性價比極高了。
為什么需要有CPU多級緩存
- 各種寄存器,用來存儲本地變量和函數參數,訪問一次需要1cycle,耗時小于1ns;
- L1 Cache,一級緩存,本地 core 的緩存,分成 32K 的數據緩存 L1d 和 32k 指令緩存 L1i,訪問 L1 需要3cycles,耗時大約 1ns;
- L2 Cache,二級緩存,本地 core 的緩存,被設計為 L1 緩存與共享的 L3 緩存之間的緩沖,大小為 256K,訪問 L2 需要 12cycles,耗時大約 3ns;
- L3 Cache,三級緩存,在同插槽的所有 core 共享 L3 緩存,分為多個 2M 的段,訪問 L3 需要 38cycles,耗時大約 12ns;
大致可以得出結論,緩存層級越接近于 CPU core,容量越小,速度越快,當 CPU 執(zhí)行運算的時候,它先去 L1 查找所需的數據,再去 L2,然后是 L3,最后如果這些緩存中都沒有,所需的數據就要去主內存拿。走得越遠,運算耗費的時間就越長。
什么是緩存行
緩存行 (Cache Line) 便是 CPU Cache 中的最小單位,CPU Cache 由若干緩存行組成,一個緩存行的大小通常是 64 字節(jié)(這取決于 CPU),并且它有效地引用主內存中的一塊地址。一個 Java 的 long 類型是 8 字節(jié),因此在一個緩存行中可以存 8 個 long 類型的變量。

猜一下下面代碼的執(zhí)行時間:
- public class ArrayLoop {
- public static void main(String[] args) {
- long[][] arr = new long[1024 * 1024][8];
- long sum = 0;
- //橫向遍歷
- long start = System.currentTimeMillis();
- for (int i = 0; i < 1024 *1024; i++) {
- for (int j = 0; j < 8; j++) {
- sum += arr[i][j];
- }
- }
- System.out.println("橫向遍歷耗時:" + (System.currentTimeMillis() - start) + "ms");
- //縱向遍歷
- start = System.currentTimeMillis();
- for (int i = 0; i < 8; i++) {
- for (int j = 0; j < 1024 * 1024; j++) {
- sum += arr[j][i];
- }
- }
- System.out.println("縱向遍歷耗時:" + (System.currentTimeMillis() - start) + "ms");
- }
- }
在我電腦上的執(zhí)行時間為:
橫向遍歷耗時:32ms
縱向遍歷耗時:88ms
在程序運行的過程中,緩存每次更新都從主內存中加載連續(xù)的 64 個字節(jié)。因此,如果訪問一個 long 類型的數組時,當數組中的一個值被加載到緩存中時,另外 7 個元素也會被加載到緩存中。
什么是偽共享

如果多個線程的變量共享了同一個 CacheLine,任意一方的修改操作都會使得整個 CacheLine 失效(因為 CacheLine 是 CPU 緩存的最小單位),也就意味著,頻繁的多線程操作,CPU 緩存將會徹底失效,降級為 CPU core 和主內存的直接交互。
如何避免偽共享
使用了字節(jié)填充技術(空間換時間)解決偽共享;
- public final class FalseSharing implements Runnable {
- public final static int NUM_THREADS = 4; // change
- public final static long ITERATIONS = 500L * 1000L * 1000L;
- private final int arrayIndex;
- private static VolatileLong[] longs = new VolatileLong[NUM_THREADS];
- static {
- for (int i = 0; i < longs.length; i++) {
- longs[i] = new VolatileLong();
- }
- }
- public FalseSharing(final int arrayIndex) {
- this.arrayIndex = arrayIndex;
- }
- public static void main(final String[] args) throws Exception {
- final long start = System.currentTimeMillis();
- runTest();
- System.out.println("duration = " + (System.currentTimeMillis() - start));
- }
- private static void runTest() throws InterruptedException {
- Thread[] threads = new Thread[NUM_THREADS];
- for (int i = 0; i < threads.length; i++) {
- threads[i] = new Thread(new FalseSharing(i));
- }
- for (Thread t : threads) {
- t.start();
- }
- for (Thread t : threads) {
- t.join();
- }
- }
- public void run() {
- long i = ITERATIONS + 1;
- while (0 != --i) {
- longs[arrayIndex].value = i;
- }
- }
- public final static class VolatileLong {
- public volatile long value = 0L;
- public long p1, p2, p3, p4, p5, p6; // 填充,可以注釋后對比測試
- }
- }
Java8 中實現字節(jié)填充
- @Retention(RetentionPolicy.RUNTIME)
- @Target({ElementType.FIELD, ElementType.TYPE})
- public @interface Contended {
- String value() default "";
- }
注意需要同時開啟 JVM 參數:-XX:-RestrictContended=false
@Contended 注解會增加目標實例大小,要謹慎使用。默認情況下,除了 JDK 內部的類,JVM 會忽略該注解。要應用代碼支持的話,要設置 -XX:-RestrictContended=false,它默認為 true(意味僅限 JDK 內部的類使用)。當然,也有個 –XX: EnableContented 的配置參數,來控制開啟和關閉該注解的功能,默認是 true,如果改為 false,可以減少 Thread 和 ConcurrentHashMap 類的大小。參加《Java性能權威指南》210 頁。
ConcurrentHashMap 中,使用 @sun.misc.Contended 對靜態(tài)內部類 CounterCell 進行修飾。另外還包括并發(fā)容器 Exchanger 也有相同的操作。
- /* ---------------- Counter support -------------- */
- /**
- * A padded cell for distributing counts. Adapted from LongAdder
- * and Striped64. See their internal docs for explanation.
- */
- @sun.misc.Contended static final class CounterCell {
- volatile long value;
- CounterCell(long x) { value = x; }
- }
Thread 線程類的源碼中,使用 @sun.misc.Contended 對成員變量進行修飾。
- // The following three initially uninitialized fields are exclusively
- // managed by class java.util.concurrent.ThreadLocalRandom. These
- // fields are used to build the high-performance PRNGs in the
- // concurrent code, and we can not risk accidental false sharing.
- // Hence, the fields are isolated with @Contended.
- /** 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;
參考:
https://juejin.im/post/5c471d75e51d45299a08b333
https://juejin.im/post/5cd644886fb9a032136fe6d7