volatile无法保证复合操作原子性,因其仅保障单次读/写操作的可见性与有序性,而count++等“读—改—写”操作会被JVM拆解为多步指令,线程可在此间交错执行,导致丢失更新;synchronized则通过互斥锁将整个操作封装为不可分割的临界区,确保原子性。

volatile 无法替代 synchronized 保证复合操作原子性,根本原因在于:它只干预单次读或写动作的内存行为,不控制多步操作的执行流程。
volatile 只管“一次读”或“一次写”
它确保变量的每次读取都从主内存加载最新值,每次写入都立即刷回主内存,并禁止相关指令重排序。但这个保障仅作用于“一个独立的读操作”(如 if (done))或“一个独立的写操作”(如 flag = true)。只要操作涉及“读—改—写”三步,volatile 就不再起作用。
复合操作在 JVM 层面天然不是原子的
像 count++、value += 1、list.add(x) 这类操作,在字节码层面会被拆成多个指令:
- 从主内存或工作内存中读取当前值
- 在 CPU 寄存器中完成计算(如加 1)
- 把结果写回变量所在内存位置
volatile 不会阻止其他线程在这三步之间插入执行。比如线程 A 读取了 count=5,正准备加 1;此时线程 B 也读取了 count=5,也加 1 并写回 6;接着线程 A 再写回 6 —— 最终结果仍是 6,而不是预期的 7。这就是典型的丢失更新。
立即学习“Java免费学习笔记(深入)”;
synchronized 通过锁把多步变成“不可分割的整体”
它不依赖变量修饰,而是对一段代码加互斥保护:
- 进入 synchronized 块前必须获取锁,未获取则阻塞
- 持有锁期间,其他线程无法进入同一把锁保护的临界区
- 整个代码块(含读、算、写)被当作一个逻辑单元执行,不会被中断或交错
- 退出时自动刷新共享变量到主内存,同时保证可见性
所以 count++ 放进 synchronized 方法里,就等效于“锁定→读→加 1→写→解锁”由单一线程独占完成,彻底规避竞态条件。
什么时候必须用 synchronized(或原子类),不能靠 volatile?
出现以下任一情况,就说明操作超出了 volatile 的能力边界:
- 计数器累加(i++、sum += x)
- 集合结构变更(ArrayList.add()、HashMap.put())
- 状态判断后伴随修改(如“若未初始化则创建实例”)
- 多个变量需同步更新(如 balance 和 lastTransactionTime 必须成对变更)
判断标准很直接:只要该操作需要不止一次内存访问,且中间状态会影响后续逻辑或对外可见,volatile 就无能为力。


















