侧边栏壁纸
  • 累计撰写 142 篇文章
  • 累计创建 21 个标签
  • 累计收到 3 条评论

目 录CONTENT

文章目录

CPU Cache、MESI 与伪共享

YaFuX
2023-10-01 / 0 评论 / 0 点赞 / 2 阅读 / 0 字
温馨提示:
部分素材来自网络,若不小心影响到您的利益,请联系我们删除。

前言

现代 CPU 已经非常快了,但打开处理器结构图,却会发现一个很奇怪的现象:

寄存器
  ↓
L1 Cache
  ↓
L2 Cache
  ↓
L3 / LLC
  ↓
内存
  ↓
SSD / 硬盘

为什么要搞这么多层?

如果 CPU 需要读取一个 4 字节的 int,为什么硬件通常不是只搬这 4 个字节,而是把它所在的一整条 Cache Line 一起搬进来?

更奇怪的是,多线程程序里可能出现这种情况:

thread1 -> 修改 x
thread2 -> 修改 y

xy 明明是两个完全不同的变量,没有共享同一个字段,为什么两个线程仍然可能互相拖慢?

继续往下还会遇到更多问题:

  • L1、L2、L3 到底解决了什么?
  • 程序局部性为什么能让 Cache 有效?
  • Cache Line 为什么常见是 64 字节?
  • 多个 CPU Core 各自缓存同一个内存地址后,谁说了算?
  • MESI 到底保证了什么?它是不是已经解决了 Java 多线程可见性?
  • “缓存锁”和“总线锁”到底是什么关系?
  • False Sharing 为什么叫“伪共享”?
  • volatile@Contended 和 Disruptor 又是怎么和 Cache Line 联系起来的?

这篇文章就沿着一条链路把这些问题串起来:

CPU 比内存快
    ↓
必须引入 Cache
    ↓
Cache 按块读取
    ↓
出现 Cache Line
    ↓
多核各自持有副本
    ↓
需要 Cache Coherence
    ↓
MESI
    ↓
多个独立变量可能落在同一行
    ↓
False Sharing
    ↓
Padding / @Contended

一、为什么需要多级缓存?

先假设世界上没有 Cache:

CPU → 内存 → CPU → 内存 → CPU → 内存

CPU 每做一点计算都必须等内存返回数据。

问题在于,处理器执行能力增长以后,CPU 内部运算和外部 DRAM 访问之间出现了巨大的延迟差距。

如果处理器大部分时间都在:

等数据
等数据
等数据

再强的 ALU 也没有意义。

1. CPU 与内存的速度差

于是出现了缓存。

CPU
 ↓
很小、很快的存储
 ↓
再大一点、再慢一点
 ↓
更大、更慢
 ↓
主内存

如果把这个思想不断重复,就形成了存储器层次结构。

1.1 为什么不能把所有存储都做得和寄存器一样快?

如果只考虑速度,最理想的机器当然是:

所有数据
   ↓
全部放在 CPU 内部超高速存储

但现实还要考虑:

  • 芯片面积;
  • 功耗;
  • 容量;
  • 制造成本;
  • 布线和访问复杂度。

越靠近 CPU 执行核心、越追求极低延迟的存储,单位容量通常越昂贵。

所以计算机最终形成的不是“全都最快”,而是一套工程折中:

少量超快存储 + 更多较快存储 + 大量较慢存储。

2. 存储器层次结构

一个适合建立直觉的模型是:

寄存器
L1 Cache
L2 Cache
L3 / Last Level Cache
DRAM 主内存
SSD
机械硬盘 / 远程存储

越往上,通常:

容量更小
成本更高
延迟更低

越往下则反过来。

2.1 延迟数字为什么不能死记?

很多资料为了建立直觉,会给出类似:

Register < L1 < L2 < L3 < DRAM

再给每一级配一个固定纳秒数。

顺序本身有意义,但具体数字不要死记。

访问延迟会受到很多因素影响:

  • CPU 微架构;
  • 时钟频率;
  • Cache 容量和组织方式;
  • NUMA 拓扑;
  • 内存频率和时序;
  • 是否命中;
  • 是否发生 TLB Miss;
  • 是否需要跨核心 / 跨 Socket 获取数据。

真正应该记住的是数量级和相对关系:

层级典型位置核心特点
RegisterCPU Core 内部容量极小,直接服务指令执行
L1Core 内部最靠近执行单元的 Cache
L2Core 或 Core Cluster 附近更大,通常比 L1 慢
LLC / L3多核共享或分片共享容量更大,拓扑依 CPU 而异
DRAMCPU 外部内存系统容量大,但延迟明显更高
SSD / HDD持久化存储容量更大,延迟又高几个数量级

不同 CPU 的实际 Cache 拓扑并不完全一样,因此“每个核一定独享 L1/L2、所有核一定共享一个完整 L3”也只能当常见模型,不能当所有架构的硬规则。

3. 局部性与 Cache Line

假设程序执行:

int x = array[100];

直觉上只需要 4 字节。

但 CPU Cache 通常不会围绕 Java 变量的大小来搬数据,而是以固定粒度进行数据填充和一致性管理。

这就是 Cache Line。

3.1 为什么按块读取更快?

关键是程序局部性。

局部性通常分成两个方向。

时间局部性

刚刚访问过的数据,很可能很快再次访问。

例如循环里的计数器:

for (int i = 0; i < 1_000_000; i++) {
    sum += i;
}

sumi 会被反复使用。

空间局部性

访问某个地址后,很可能继续访问附近地址。

例如:

for (int i = 0; i < array.length; i++) {
    sum += array[i];
}

读取:

array[0]

之后,大概率就是:

array[1]
array[2]
array[3]
...

既然如此,与其每次只从更慢的一层拿几个字节,不如一次把附近数据一起搬进来。

3.2 Cache Line:缓存的基本管理粒度

在很多现代 x86 处理器中,Cache Line 常见大小是:

64 Bytes

假设有一个 64 字节对齐的区间:

0x1000 ~ 0x103F

程序只读:

0x1008

CPU 也可能把整个:

0x1000 ~ 0x103F

对应的 Cache Line 填充进 Cache。

于是下一次读取附近地址时,很可能直接命中,不必重新访问更慢的层级。

3.3 为什么常见是 64 字节?

不能把:

Cache Line 永远等于 64B

当成通用真理。

64B 在现代 x86 等很多平台上非常常见,但不同体系结构和具体处理器可以使用不同粒度。

写业务程序时,通常不应该依赖硬编码的 Cache Line 大小。

4. Cache Line、Page 与 Disk Block

它们有一个共同思想:

都在某个层面使用“块”而不是单个 bit / byte 作为管理或传输粒度。

但不能因此说:

Cache Line = Page = Disk Block

它们属于不同层级。

概念管理层级常见作用
Cache LineCPU CacheCache 填充、一致性等
Memory PageMMU / OS 虚拟内存地址映射、权限、换页
Filesystem / Disk Block文件系统 / 存储设备持久化数据组织和 I/O

例如:

Cache Line 常见几十字节级
Memory Page 常见 4KiB,也可能使用 Huge Page
文件系统块大小则取决于文件系统和配置

所以它们只是共享“分块管理”的思想,语义和大小完全不同。

5. DMA:让设备直接与内存传输

当网卡、SSD、GPU 等设备要和内存交换大量数据时,如果每个字节都必须:

设备
 ↓
CPU 寄存器
 ↓
内存

CPU 会变成昂贵的数据搬运工。

DMA,也就是 Direct Memory Access,要解决的就是这个问题。

一个简化流程可以表示为:

MemoryI/O DeviceCPUMemoryI/O DeviceCPU配置传输参数 / 描述符启动 I/ODMA 读写一批数据中断 / 完成通知

关键点不是“CPU 完全不知道这次传输”,而是:

CPU 负责配置和协调,但大批量数据不必一个字节一个字节经过 CPU 通用寄存器搬运。

现代系统中还可能涉及 IOMMU、设备队列、PCIe DMA 等更多细节,这里先不继续展开。

二、多核缓存一致性

1. 多核为什么会出现一致性问题?

假设内存里:

x = 0

Core 0 读取了 x

Core 0 L1: x = 0

Core 1 也读取:

Core 1 L1: x = 0

现在同一个内存位置出现了多个缓存副本。

如果 Core 0 修改:

x = 1

Core 1 手里的:

x = 0

就不能继续被当成最新值。

于是多核 CPU 必须解决:

多个 Cache 中同一个内存位置的副本,怎样保持一致?

这就是 Cache Coherence 要处理的问题。

2. MESI 协议

MESI 是一种经典的 Cache Coherence 协议模型。

名字来自四个状态:

M = Modified
E = Exclusive
S = Shared
I = Invalid

可以先用非常直观的方式理解。

2.1 Exclusive

某个 Core 拿到一条 Cache Line,并且目前没有其他 Core 持有这条 Line 的有效副本。

它可以处于:

E

2.2 Shared

另一个 Core 也读取了同一条 Line。

于是两边都可以拥有只读有效副本:

Core 0: S
Core 1: S

2.3 Modified

如果 Core 0 想写这条 Line,它必须获得写权限 / 所有权。

其他 Core 对应副本会被失效。

最终可以形成类似:

Core 0: M
Core 1: I

2.4 Invalid

Core 1 的副本已经不能再使用:

I

如果 Core 1 下次还要读,就需要重新获得最新的 Cache Line。

一个简化过程:

Coherence DomainCore 1Core 0Coherence DomainCore 1Core 0两边都可能以 Shared 持有获得写权限并修改Read Line XLine XRead Line XLine X请求写入 Line XInvalidate Line XACK再次读取 Line X返回最新数据

2.5 Invalid 后的数据从哪里来?

不一定。

这也是很多入门图里容易简化过头的地方。

最新数据可能来自:

  • 其他 Core 的 Cache;
  • Last Level Cache;
  • 一致性互连中的其他层级;
  • 最终才可能访问 DRAM。

现代 CPU 的 Cache Coherence 是一个完整的硬件一致性系统,不是简单的:

失效
 ↓
永远去主内存重新读

3. MESI 与 Java 内存语义

这是理解并发时最关键的分界线之一。

MESI 解决的是:

硬件 Cache 副本的一致性。

Java Memory Model 解决的是:

Java 线程之间什么写入必须对什么读取可见,以及操作之间允许怎样重排序。

两者不是一个层级。

现代程序执行链里还存在:

Java 编译器
   ↓
JIT
   ↓
CPU 指令
   ↓
Store Buffer / Load Queue
   ↓
Cache Coherence

即使硬件能够保证某个内存位置最终具有一致值,也不代表:

线程 A 的任意写

一定立刻按照你写代码的顺序,被:

线程 B

观察到。

编译器和 CPU 都可能做合法的重排序。

因此 Java 还需要:

  • volatile
  • synchronized
  • 锁;
  • 原子类;
  • happens-before 规则;

来建立语言层面的可见性和有序性语义。

可以把两者关系理解成:

Java Memory Model
编译器 / JIT 约束
内存屏障 / 原子指令
CPU Memory Ordering
Cache Coherence

所以:

有 MESI,不等于 Java 多线程自动线程安全。

这也是为什么 Cache Coherence 和 Java Memory Model 必须分开理解。

4. Cache Lock 与 Bus Lock

很多资料会给出这样一个模型:

能用 MESI → 缓存锁
不能用 MESI → 总线锁

它能让人知道“锁整个总线很贵”,但现代 x86 的实际机制不能简单概括成:

数据超过一个 Cache Line,就一定锁总线。

4.1 现代 x86 的 LOCK 如何工作?

从 P6 以后的 Intel 处理器开始,对于内部可缓存的内存区域,带 LOCK 的原子操作一般不会直接拉起传统意义上的 LOCK# 总线信号。

更常见的情况是:

锁定 / 独占相关 Cache Line
        +
Cache Coherence 协议
        ↓
保证该原子操作

这比阻止所有处理器访问内存总线高效得多。

4.2 哪些场景仍可能出现 Bus Lock?

在某些特殊条件下,例如某些跨 Cache Line 的 Split Lock 或访问不可缓存内存区域,处理器仍可能需要更重的总线级机制。

所以应该建立的正确认知是:

现代处理器优先利用 Cache 与 Coherence 完成原子性;Bus Lock 是更特殊、更昂贵的路径,而不是所有“跨一行数据”的统一答案。

三、False Sharing 与数据布局

1. False Sharing 是怎么产生的?

现在来看这篇文章最有意思的部分。

假设同一条 64B Cache Line 里刚好有:

┌────────────────────────────────────────────────────┐
│ x │ y │ 其他数据 ...                               │
└────────────────────────────────────────────────────┘

两个线程分别运行在两个 Core:

Core 0 → 不停写 x
Core 1 → 不停写 y

从 Java 变量角度:

x != y

它们完全是两个独立字段。

但从 Cache Coherence 角度,同步粒度是:

Cache Line

而不是 Java 字段。

于是可能发生:

Core 0 获得这条 Line 的写权限
          ↓
Core 1 对应 Line 失效
          ↓
Core 1 又要写 y
          ↓
Core 1 获得这条 Line 的写权限
          ↓
Core 0 对应 Line 失效
          ↓
反复 Ping-Pong

这就是 False Sharing。

之所以叫“伪共享”,是因为:

逻辑上两个线程没有共享同一个变量,但物理上它们共享了同一条 Cache Line。

1.1 False Sharing 的触发条件

至少要出现类似情况:

  • 两个不同 Core 频繁访问同一条 Cache Line;
  • 至少一方频繁写入;
  • 两个位置本来可以彼此独立,却因为 Cache Line 粒度产生额外一致性流量。

如果大家只是读:

Shared

通常不会出现这种反复争抢写权限的问题。

2. 实验:观察 @Contended 的影响

下面使用 OpenJDK 21 做一个简单实验。

这不是严谨的微基准,只是为了观察数据布局改变后可能出现的差异。

2.1 示例代码

import jdk.internal.vm.annotation.Contended;

public class FalseSharing {

    private static final long ITERATIONS = 50_000_000L;

    static class Pair {
        volatile long x;
        volatile long y;
    }

    static class ContendedPair {
        @Contended("x")
        volatile long x;

        @Contended("y")
        volatile long y;
    }

    private static long runPair() throws InterruptedException {
        Pair pair = new Pair();

        Thread t1 = new Thread(() -> {
            for (long i = 0; i < ITERATIONS; i++) {
                pair.x = i;
            }
        });

        Thread t2 = new Thread(() -> {
            for (long i = 0; i < ITERATIONS; i++) {
                pair.y = i;
            }
        });

        long start = System.nanoTime();

        t1.start();
        t2.start();

        t1.join();
        t2.join();

        return System.nanoTime() - start;
    }

    private static long runContended() throws InterruptedException {
        ContendedPair pair = new ContendedPair();

        Thread t1 = new Thread(() -> {
            for (long i = 0; i < ITERATIONS; i++) {
                pair.x = i;
            }
        });

        Thread t2 = new Thread(() -> {
            for (long i = 0; i < ITERATIONS; i++) {
                pair.y = i;
            }
        });

        long start = System.nanoTime();

        t1.start();
        t2.start();

        t1.join();
        t2.join();

        return System.nanoTime() - start;
    }

    public static void main(String[] args) throws Exception {
        for (int i = 0; i < 3; i++) {
            double normal = runPair() / 1_000_000.0;
            double contended = runContended() / 1_000_000.0;

            System.out.printf(
                    "pair=%.1f ms, contended=%.1f ms%n",
                    normal,
                    contended
            );
        }
    }
}

2.2 编译参数

在现代 JDK 中:

jdk.internal.vm.annotation.Contended

属于 JDK 内部 API,不是标准 Java SE 公共 API。

因此编译时需要显式导出:

javac \
  --add-exports java.base/jdk.internal.vm.annotation=ALL-UNNAMED \
  FalseSharing.java

这条命令验证的是:

允许当前未命名模块访问 java.base 中的内部 jdk.internal.vm.annotation 包。

2.3 运行参数

执行:

java \
  --add-exports java.base/jdk.internal.vm.annotation=ALL-UNNAMED \
  -XX:-RestrictContended \
  FalseSharing

-XX:-RestrictContended 的作用,是允许 HotSpot 对普通用户类中的 @Contended 生效。

否则为了防止应用随意浪费大量内存,VM 默认会限制这个注解的使用范围。

2.4 实验结果与分析

在 OpenJDK 21.0.11 的一次测试环境中,得到过:

pair=484.9 ms, contended=101.0 ms
pair=67.9 ms, contended=13.1 ms
pair=73.2 ms, contended=12.2 ms

第一轮明显更慢,已经在提醒我们:

这不是可以拿来做性能结论的严谨 Benchmark。

JIT 预热、线程调度、CPU 频率、核心绑定、系统负载都会改变结果。

这个实验真正要观察的是:

普通相邻字段
     ↓
可能位于同一 Cache Line
     ↓
两个 Core 高频写
     ↓
Coherence Ping-Pong

vs

@Contended
     ↓
VM 插入隔离 / Padding
     ↓
降低独立热点字段共享同一 Cache Line 的概率

如果要做正式性能测试,应该使用 JMH,而不是直接拿一次 System.nanoTime() 的结果当结论。

3. volatile 与 False Sharing

不是。

这是一个非常容易被实验代码误导的地方。

False Sharing 的根本原因是:

多个热点内存位置落在同一 Cache Line,并被不同 Core 高频访问,其中存在写操作。

即使没有 volatile,硬件层面仍然存在 Cache Line 和 Coherence。

实验中使用 volatile,更多是为了让这些写操作具有明确的 Java 可见性语义,同时降低 JIT 把循环写入合并、消除等优化对演示造成的干扰。

因此:

volatile ≠ False Sharing

更不能理解成:

加了 volatile 才会触发 MESI。

Cache Coherence 是硬件机制,不是 Java 关键字打开的功能。

4. @Contended 的实现思路

JEP 142 在 JDK 8 引入了 @Contended 这套机制,目标就是:

让 VM 知道某些字段可能会在不同处理器核心上发生高竞争,从而安排对象布局,尽量避免这些字段和其他独立热点字段共享 Cache Line。

可以把普通布局想象成:

| header | x | y |

使用隔离后,逻辑上更接近:

| header | padding ... | x | padding ... | y | padding ... |

具体对象布局和 Padding 策略属于 VM 实现细节。

4.1 为什么手动 Padding 不够可靠?

以前常见手工 Padding:

long p1, p2, p3, p4, p5, p6, p7;
volatile long value;
long p9, p10, p11, p12, p13, p14, p15;

想法是:

7 × 8B = 56B

再加上目标 long 的 8B,凑到大约 64B。

这个思路能帮助理解,但它不是一个可靠的 Java 语言保证。

原因包括:

  • 对象头会占空间;
  • 字段布局由 JVM 决定;
  • 对齐规则会变化;
  • Cache Line 大小并非所有平台都相同;
  • VM 可能重排字段布局。

所以手写 Padding 更像针对已知运行时和硬件做的底层优化,而不是普通业务代码应该依赖的通用技巧。

4.2 @Contended 的适用边界

它依然是 HotSpot / JDK 实现层面的能力。

因此更合理的态度是:

理解它解决什么问题,比在业务对象上到处加注解更重要。

它会用更多内存换取更低的 Cache Coherence 竞争,所以只适合真正的高竞争热点数据结构。

5. Disruptor 中的实际应用

LMAX Disruptor 是理解这个问题非常好的真实案例。

Disruptor 中的 Sequence 是一个会被高频读写的核心序列值。

它的实现专门考虑了 Padding,以降低这个热点值和其他独立数据发生 False Sharing 的概率。

为什么框架愿意这么做?

因为这类底层并发组件追求的是:

极高频率
极低延迟
极少竞争

多浪费几十或几百字节内存,可能换来更稳定的多核扩展能力。

但普通业务对象完全不是这个量级。

所以不要因为 Disruptor 做了 Padding,就把所有 Java Bean 都 Padding 一遍。

优化应该建立在真实 Profiling 和 Benchmark 上,而不是根据一个“底层技巧”提前猜测瓶颈。

四、常见误区

1. MESI 与一致性相关误区

误区 1:有 MESI,多线程就安全了

错误。

MESI 解决硬件 Cache Coherence;Java 线程安全还涉及 JMM、原子性、可见性、有序性以及同步语义。

误区 2:一个变量失效后一定回 DRAM 读取

错误。

最新 Cache Line 可能从其他 Core、共享 LLC 或其他一致性路径获得。

2. Cache Line 与数据布局相关误区

误区 3:Cache Line 永远是 64 字节

错误。

64B 很常见,尤其在现代 x86 平台,但不是跨所有 CPU 的规范常量。

误区 4:两个线程修改两个不同变量,就不存在竞争

错误。

只要两个变量落入同一个 Cache Line,并且不同 Core 高频写,就可能出现 False Sharing。

误区 5:volatile 会制造 False Sharing

错误。

volatile 是 Java 内存语义;False Sharing 的根因是数据布局和 Cache Coherence 粒度。

误区 6:7 个 long Padding 一定能让变量独占一行

错误。

对象头、字段布局、对齐和硬件 Cache Line 大小都可能让这个假设失效。

3. 原子操作与存储层级相关误区

误区 7:原子指令加 LOCK 就等于锁内存总线

不准确。

现代 x86 对可缓存内存通常利用 Cache Lock / Coherence 完成原子性,真正的 Bus Lock 属于更特殊而昂贵的场景。

误区 8:Cache Line、Memory Page、Disk Block 只是同一个东西换了名字

错误。

它们虽然都有分块思想,但属于 CPU、虚拟内存和存储系统三个不同层级。

五、把整条缓存链路串起来

1. 从上一篇的 CPU 执行继续往下走

上一篇《从硅到 CPU:Java 程序执行完整链路》解决的是一个更基础的问题:

计算机到底是怎么完成一次计算的?

它建立的主线是:

硅
 ↓
晶体管
 ↓
逻辑电路
 ↓
CPU
 ↓
机器指令
 ↓
JVM
 ↓
Java 程序执行

但当 CPU 真正开始执行程序后,会立刻遇到另一个现实问题:

CPU 很快,内存却跟不上。怎么办?

于是这一篇继续从 CPU 往存储体系深入:

CPU 执行程序
    ↓
CPU 与内存存在速度差
    ↓
引入多级 Cache
    ↓
Cache 按 Cache Line 管理数据
    ↓
多核产生多个缓存副本
    ↓
需要 Cache Coherence
    ↓
MESI 等一致性协议
    ↓
以 Cache Line 为粒度又可能产生 False Sharing

所以这两篇文章不是两个孤立主题,而是一条连续的硬件执行链:

硅与晶体管
数字逻辑
CPU
机器指令执行
寄存器与 Cache
多级 Cache
Cache Line
多核缓存副本
Cache Coherence / MESI
False Sharing

第一篇回答“CPU 为什么能够计算”,这一篇继续回答“CPU 怎样尽量减少等待内存,以及多核缓存因此产生了什么新问题”。

到这里,整个认知链已经从最底层的晶体管,一直走到了 Java 程序在多核处理器上的缓存行为。

2. 一次 Cache 访问的完整流程

假设 Core 0 第一次读取变量 x

Yes
No
Yes
No
Yes
No
No
Yes
Core 0 Load x
L1 Hit?
直接获得 Cache Line
L2 Hit?
填充到更近层级
LLC Hit?
访问更远的一致性域 / DRAM
CPU 使用 x
Core 1 也读取同一 Line
多个 Core 持有有效副本
Core 0 写 x
请求该 Line 写权限
其他副本失效
Core 0 写入
同一 Line 里还有 Core 1 高频写的 y?
正常继续
Line 在 Core 间频繁转移
False Sharing
Padding / @Contended / 重新设计数据布局

把这张图理解了,Cache Line、MESI 和 False Sharing 就不再是三个孤立的面试名词。

它们其实是同一个因果链上的三个阶段:

为了快 → 引入 Cache
为了提高命中 → 按 Line 搬
为了多核正确 → 做 Coherence
因为 Coherence 以 Line 为粒度 → 产生 False Sharing
为了减少 False Sharing → 改变数据布局

六、总结

CPU Cache 的本质,是用空间和复杂度换时间。

多级 Cache 让 CPU 不必每次都等待 DRAM;程序局部性让“多搬一点附近数据”通常是值得的;Cache Line 又成为 Cache 填充和一致性管理的重要粒度。

但多核之后,同一个内存位置可以同时存在多个缓存副本,于是硬件必须引入 Cache Coherence。

MESI 正是在这一层工作。

而一旦 Coherence 的粒度是整条 Cache Line,就会出现一个有趣的副作用:

两个完全不同的变量
        ↓
恰好处于同一 Cache Line
        ↓
不同 Core 高频写
        ↓
整条 Line 反复转移
        ↓
False Sharing

这也是 Padding@Contended 和 Disruptor 数据布局优化背后的真正原因。

Cache 解决的是 CPU 与内存的速度差,MESI 解决的是多核 Cache 副本的一致性,而 False Sharing 则是“以 Cache Line 为一致性粒度”带来的性能副作用;理解这三层关系之后,Java 中很多看似神秘的底层并发优化就有了清晰的硬件起点。

0

评论区