Java多线程内存模型基于JMM抽象,受volatile/synchronized约束,底层依赖MESI协议及Store Buffer/Invalidation Queue;x86强模型难复现可见性问题,ARM常见;volatile通过内存屏障强制刷缓存并禁止重排,但不保证原子性;synchronized退出时刷新所有变量并触发完整屏障序列;JMM屏蔽硬件细节,由JVM与CPU协同实现顺序一致性。

Java多线程看到的“内存”不是物理内存
Java线程读写的不是CPU缓存里的最新值,而是JVM抽象出的主内存视图——这个视图受volatile、synchronized和final字段语义约束,但底层仍跑在MESI协议上。MESI本身不保证跨核的写立即可见,它只管缓存行状态;而CPU为了性能加了Store Buffer和Invalidation Queue,这两者才是乱序的真正推手。
常见错误现象:x = 1; y = 1;在两个线程分别执行后,另一个线程可能观察到x == 1 && y == 0或x == 0 && y == 1——这在x86上极难复现(因为有强内存模型),但在ARM/AArch64上很常见。
- 不要依赖“写完就立刻能被其他核读到”,哪怕只是普通int赋值
-
System.out.println会隐式触发内存屏障(尤其在HotSpot中),不能用来验证可见性 - JIT编译可能把无同步的循环变量优化成寄存器常量,导致死循环(如经典的
while (!flag) {})
volatile关键字如何绕过Store Buffer
volatile写操作会插入StoreLoad屏障(x86下是lock addl $0, (%%rsp)这类指令),强制刷空当前CPU的Store Buffer,并等待所有失效请求被处理完毕;读操作则插入LoadLoad和LoadStore屏障,确保后续读写不会重排到它前面。
使用场景:状态标志、单次发布(double-checked locking中的instance)、轻量级信号量。
立即学习“Java免费学习笔记(深入)”;
-
volatile不能保证复合操作原子性(counter++仍是三步) - 在ARMv7+上,
volatile读对应ldar,写对应stlr,语义比x86更接近Java规范 - 不要用
volatile替代AtomicInteger做计数——后者还提供CAS等原子操作
synchronized块实际触发的缓存动作
退出synchronized块时,JVM要求将本线程本地修改的全部volatile和非volatile变量刷新回主内存(JLS 17.4),这背后是完整的内存屏障序列:包括清空Store Buffer、等待Invalidation Queue完成、并广播Invalidate消息。
性能影响:高竞争下,频繁进出锁会导致大量缓存行在核间反复迁移(cache line bouncing),比单纯用volatile贵一个数量级。
- 锁的粒度越小越好,但别小到让MESI开销占主导(比如每个元素一把锁)
- 偏向锁和轻量级锁在无竞争时几乎不触发缓存同步,但一旦升级为重量级锁,就会走系统互斥量,强制跨核同步
-
synchronized(this)和synchronized(staticObj)触发的是不同缓存行的MESI状态流转,别混用以为能“共享同步点”
为什么JMM不直接暴露MESI状态
因为MESI是硬件协议,而Java内存模型(JMM)是语言级抽象——它只要求“正确同步的程序”表现出顺序一致性(SC),至于怎么实现,由JVM和CPU共同协商。HotSpot在x86上利用其强模型省掉部分屏障,在ARM上则补足ldar/stlr来模拟SC语义。
容易被忽略的地方:即使你写了正确的同步,如果JVM运行在虚拟化环境中(如KVM + host-passthrough未开启),CPU特性可能被截断,导致volatile语义降级;或者容器里限制了cpuset,使线程被调度到同一物理核的不同超线程上,此时MESI不起作用(共用L1 cache),反而放大伪共享风险。


















