主内存和工作内存是jmm抽象概念,非jvm堆栈别名;主内存对应线程共享变量真实值,工作内存是线程私有缓存副本,局部变量不受jmm约束。

主内存和工作内存不是JVM堆和栈的别名
很多人一看到“主内存”就去翻JVM内存结构图,以为它等于堆,“工作内存”等于虚拟机栈——这是最典型的误解。主内存是JMM抽象出来的共享数据源概念,对应的是所有线程可见的变量真实值(比如堆里某个对象的int count字段);工作内存则是每个线程私有的副本缓存区,它可能落在CPU寄存器、L1/L2缓存,甚至部分在栈帧里,但**不等于整个栈**。
关键区别在于:局部变量、方法参数从不进入主内存,它们天生线程私有,JMM根本不关心;而实例字段、静态字段、数组元素才受JMM规则约束。
- 错误现象:
i++在多线程下结果小于预期 → 因为i被各线程各自加载副本,修改后没同步回主内存 - 实操建议:调试时别盯着
javap -v看栈帧大小,要关注变量是否被volatile修饰、是否在synchronized块内、或是否用AtomicInteger - 容易踩的坑:把
final int x = 5;当成普通变量处理——其实final字段在构造完成那一刻就对其他线程可见,这是JMM对不可变性的特殊保障,和volatile机制不同
read/load/use/assign/store/write 这8个操作不是Java语法,而是语义约束
JMM定义的这8种原子操作,不是你能写出来的代码,而是JVM执行引擎在背后必须保证的底层行为契约。比如你写counter++,JVM必须拆成至少read→load→use→assign→store→write六步,且每一步都不可分割——但问题来了:这些步骤之间没有天然顺序保证。
- 常见错误现象:线程A执行
a = 1; flag = true;,线程B看到flag == true却读到a == 0→ 这就是重排序+可见性缺失的典型组合 - 实操建议:想让
a = 1对B可见,不能只靠flag = true,得让flag是volatile,或者把两行包进synchronized块 - 参数差异:
volatile仅保障单变量的可见性和禁止重排序;synchronized还能提供原子性和锁互斥,但开销大;AtomicInteger用CAS实现无锁原子更新,适合计数类场景
volatile 并不能让 i++ 变成原子操作
这是面试和线上故障里最高频的误用。给int counter加volatile,确实能保证每次读都从主内存取最新值、每次写都立刻刷回主内存,但它**完全不管i++内部的三步是否被其他线程穿插执行**。
立即学习“Java免费学习笔记(深入)”;
也就是说:volatile int i = 0;之后,两个线程同时执行i++,最终结果极大概率是1,而不是2。
- 错误现象:
volatile修饰的计数器在压测中始终少于预期 → 不是可见性问题,是原子性缺失 - 实操建议:计数用
AtomicInteger,状态开关用volatile boolean,复杂逻辑用synchronized或ReentrantLock - 性能影响:
volatile写会插入store屏障,比普通写慢;AtomicInteger.incrementAndGet()在无竞争时基本是单条CPU指令,比锁快得多
happens-before 是理解JMM的真正入口,不是可选项
你不背8种操作,也能写出正确并发代码;但如果不理解happens-before规则,就永远在猜“为什么这里有时生效有时不生效”。它才是JMM对开发者暴露的、可推理的顺序契约。
比如:一个synchronized块结束,和下一个synchronized块开始之间,存在隐式的happens-before关系——这意味着前者的写一定对后者可见。而volatile变量的写与后续任意线程对该变量的读之间,也构成happens-before。
- 容易踩的坑:在
synchronized块里改了list.add(x),却在块外读list.size()→ 块外读不被happens-before覆盖,可能看不到新元素 - 实操建议:遇到“明明改了却读不到”,先检查两个操作是否落在同一个
happens-before链上;优先用volatile+happens-before组合,而不是靠Thread.sleep(100)碰运气 - 复杂点:final字段的初始化完成,与构造方法返回之间存在
happens-before,所以安全发布对象时,用final比volatile更轻量
happens-before边界定住?没有的话,就等于裸奔。










