volatile不能保证原子性,因其仅确保可见性与禁止重排序,不防止执行中断;如i++的读-改-写三步可能并发冲突,导致结果错误。

Java中volatile为什么不能保证原子性
它只管“可见性”和“禁止重排序”,不管“执行过程是否被中断”。比如i++这种操作,底层是读-改-写三步,volatile能确保每次读到最新值、写完立刻刷回主内存,但两个线程同时读到10,各自加1再写11——结果还是11,不是12。
- 典型错误现象:
volatile int count = 0;然后多线程反复执行count++,最终值远小于预期 - 适用场景:状态标志位(如
running)、单次写入后只读的配置项 - 不适用场景:需要复合操作原子性的计数、累加、条件更新(如
if (count > 0) count--) - 替代方案:用
AtomicInteger或synchronized,前者基于CAS,后者靠锁保证整个临界区原子
synchronized锁的是对象,不是代码或变量
很多人以为synchronized括号里写个int变量就锁住它了,其实它锁的是括号里表达式求值后的**对象引用**。基本类型(如int)、null、局部变量本身没有锁的概念;如果锁的是this或一个static final Object lock = new Object(),才真正起作用。
- 常见错误:在方法里new一个局部
Object然后synchronized(obj)——每个线程都拿自己的对象,完全没互斥 - 使用场景:保护共享可变状态,比如多个线程修改同一个
ArrayList,必须用同一把锁(如该列表的引用,或显式声明的锁对象) - 注意点:锁对象本身不能被重新赋值,否则锁失效;
String或Integer这类常量池对象慎用,可能意外与其他代码共用同一实例
JMM如何通过happens-before规则定义可见性边界
它不是靠实时同步,而是定义了一组“谁一定早于谁发生”的逻辑顺序。只要满足其中一条规则(比如synchronized解锁 happens-before 同一锁的加锁),JVM就保证前者的写对后者可见——中间可能经过缓存、寄存器、Store Buffer,但结果等价于同步完成。
- 关键规则包括:程序顺序规则、监视器锁规则(解锁→加锁)、volatile变量规则(写→读)、线程启动/终止规则、传递性
- 容易踩的坑:仅靠
volatile写 + 普通读,不构成happens-before;必须是volatile写 →volatile读,才能保障该写对读可见 - 性能影响:happens-before本身不强制刷新所有缓存,只约束相关操作的重排序和可见时机,比全局内存屏障轻量
为什么final字段在构造完成后天然可见
这是JMM给final字段的特殊保障:只要构造函数正常结束(没抛异常、没this逃逸),其他线程看到该对象时,一定能见到final字段的初始化值,哪怕没用volatile或锁。背后是编译器和CPU对final写插入了内存屏障。
立即学习“Java免费学习笔记(深入)”;
- 前提条件:字段声明为
final;初始化在构造函数内完成;构造过程中没让this引用逃逸(比如没在构造中启动线程或传给外部方法) - 反例:在构造函数里把
this传给静态集合,另一线程拿到后读final字段,可能看到默认值(0/false/null) - 注意:仅对
final字段有效,final引用的对象内部状态仍需自己保证线程安全
volatile读写必须配对、final不能逃逸。漏掉任一环,看似合理的代码就可能在某些CPU或JVM版本上出问题。









