本文目录导读:
- 误区一:“
synchronized保证可见性,所以写操作立即对其他线程可见” - 误区二:“
volatile保证原子性,写volatile变量是线程安全的” - 误区三:“
final字段在构造器中赋值,其他线程一定能看到正确的值” - 误区四:“
LockSupport.park()/unpark()和Object.wait()/notify()是一回事” - 误区五:
“AtomicInteger所有操作都无锁,所以性能一定比synchronized好” - 总结:从“直觉正确”到“源码正确”
这是一个很有价值的切入点,很多开发者能写出“能跑的并发代码”,但遇到高并发、高负载场景就崩盘,大多是因为陷入了某些直觉上正确、实际上危险的误区。
我把源码级别的并发理解常见误区归纳为以下 5 大核心陷阱,结合底层原理(JMM/CPU架构/操作系统)来解析:
“synchronized 保证可见性,所以写操作立即对其他线程可见”
错误直觉:进入 synchronized 块,读到的就是最新值;退出 synchronized 块,其他线程立即看到改动。
源码真相:synchronized 确实保证了进入时刷新和退出时冲刷,但这是发生在同一把锁的前提下。
- 反直觉案例:线程 A 持有锁
obj1修改变量x,线程 B 持有锁obj2读变量x,两者没有 happens-before 关系,B 可能永远看不到 A 的修改(因为锁对象不同)。 - 底层:
monitorenter和monitorexit指令会触发内存屏障(LoadLoad/StoreStore),但锁对象不同,屏障各自独立,本质是违反了 JMM 的“同步规则”——只对同一个监视器有效。 - 正确理解:锁对象就是契约的中间人,必须对同一个对象加锁,才能建立 happens-before 关系。
“volatile 保证原子性,写 volatile 变量是线程安全的”
错误直觉:既然 volatile 能保证可见性和禁止重排序,那 volatile int i++; 就是原子操作。
源码真相:i++ 在字节码层面是 3 条指令(getfield -> iconst_1 -> iadd -> putfield)。volatile 只保证每次 putfield 写入主存,但读取和写入之间,其他线程可能已经修改了该值。
- 反直觉案例:两个线程同时
volatile int count++,即使count是volatile,最终结果极大概率小于 20000。 - 底层:
volatile写前插入StoreStore屏障,写后插入StoreLoad屏障;读后插入LoadLoad和LoadStore屏障。这些屏障只解决了可见性和有序性,没有锁总线或 CPU 缓存一致性协议(如 MESI)的独占排他权。 - 正确理解:
volatile是轻量级的,只能保证对单个volatile变量的读-写操作(即x = 1)是原子的,但读-改-写(如x++)不是。
“final 字段在构造器中赋值,其他线程一定能看到正确的值”
错误直觉:对象构造完成后,final 字段就被安全发布了,其他线程看到的一定是最终值。
源码真相:构造器中的 this 引用逸出(escape) 会彻底破坏 final 的保证。
- 反直觉案例:
public class FinalEscape { final int x; static FinalEscape instance; public FinalEscape() { x = 42; instance = this; // 致命:构造函数还没结束,this 就逃逸了 } }另一个线程可能读取到
instance.x == 0(默认值),因为 JVM 可能将instance = this重排序到x=42之前。 - 底层规避:JVM 会在
final字段写之后、构造函数返回之前插入StoreStore屏障,防止final字段的写入被重排序到构造函数外。但如果 this 在构造函数中途逃逸,屏障的位置就不确定了。 - 正确理解:安全发布(如
static初始化、ConcurrentHashMap放入、volatile写引用)与final双重保障才是王道,构造器中的 this 逸出是 JVM 并发安全的“禁区”。
“LockSupport.park()/unpark() 和 Object.wait()/notify() 是一回事”
错误直觉:反正都是让线程阻塞/唤醒,LockSupport 是 wait/notify 的改进版。
源码真相:语义和协作模式完全不同,混淆会导致死锁或丢失通知(Lost Notification)。
- 差异核心:
wait/notify:基于对象监视器,必须在synchronized块内使用,顺序固定:wait释放锁 -> 其他线程notify->wait线程重新争抢锁。notify 在 wait 之前执行,通知就丢失了。park/unpark:基于线程许可(permit),不需要同步块。unpark可以提前给线程“发一个许可”,后面park时直接消费许可通过。即使 unpark 在 park 之前,也不会丢失。
- 底层:
Unsafe.park/unpark直接调用操作系统(Linux 上为pthread_cond_wait+pthread_mutex的封装,但 permit 机制用计数器实现)。 - 正确理解:
wait/notify是协作式同步原语,必须成对出现且在锁内。park/unpark是线程阻塞工具,不依赖锁,且有“先通知后等待”的安全保障。
“AtomicInteger 所有操作都无锁,所以性能一定比 synchronized 好”
错误直觉:CAS 是乐观锁,比悲观锁 synchronized 快,所以所有并发计数器都应该用 Atomic。
源码真相:高竞争下,CAS 的自旋(SpinLoop)可能比 synchronized 的阻塞更差。
- 反直觉场景:线程数 > CPU 核数,且激烈更新同一个
AtomicInteger,每次 CAS 失败后都会重试,导致:- 大量 CPU 时间被浪费在无意义的循环上(忙等待)。
- 缓存一致性风暴:每次 CAS 失败成功都会引发其他线程的缓存行失效(MESI 协议的 Invalidate Bus),导致性能断崖式下跌。
- ABA 问题的存在(虽然通常可用版本号解决)。
- 底层:
AtomicInteger用Unsafe.compareAndSwapInt实现,这是一个 CPU 级别的原子指令(如 x86 的LOCK CMPXCHG),高并发下,指令总线被锁,导致所有核心停顿(Lock Prefix 会将其他核心的锁缓存行失效)。 - 正确理解:低竞争时
Atomic>synchronized(偏向锁、轻量级锁)。高竞争时:synchronized会膨胀为重量级锁,让线程进入操作系统阻塞(wait/notify),一旦阻塞就不消耗 CPU。Atomic依然在自旋,消耗 CPU,吞吐量反而不如正确使用synchronized或LongAdder(分段 CAS)。- Atomic 不适合极度高竞争,
LongAdder或LongAccumulator才是正解(它们通过 Cell 数组分散热点,最后再 sum)。
从“直觉正确”到“源码正确”
| 常见误区 | 你以为的保证 | 源码实际保证 | 核心突破点 |
|---|---|---|---|
synchronized 全局可见 |
写后立即可见 | 同一把锁可预见 | 锁对象隔离 |
volatile 原子递增 |
安全自增 | 不保证读-改-写原子性 | 指令组合非单条 |
final 安全发布 |
所有线程看最终值 | this 不逃逸时才安全 | 构造函数重排序 |
park/unpark 与 wait/notify 类似 |
通知等待机制 | 许可机制 vs 监视器机制 | 锁依赖/顺序依赖 |
Atomic 高并发无锁总最快 |
无锁一定效率高 | 高竞争自旋开销惨重 | 忙等待 vs 阻塞 |
跳出误区的钥匙:
- 信任 JMM 规范,而非直觉:多了解
happens-before规则。 - 从 CPU 缓存一致性协议(MESI)、内存屏障(Memory Barrier)、指令重排(Reordering)的视角看问题。
- 阅读 JDK 源码(
Unsafe、AbstractQueuedSynchronizer、LockSupport),理解底层实现,而不是停留在 API 文档。
如果你对某个误区特别感兴趣(缓存一致性风暴”或“锁膨胀过程”),我可以展开讲得更深一些。
标签: 并发