Java并发编程演进:从悲观锁的妥协到 AQS 的优雅
Java并发编程演进:从悲观锁的妥协到 AQS 的优雅
在复杂的高并发业务场景中,多线程编程的核心本质只有一个:如何安全、高效地管理共享状态。从早期粗暴的重量级锁,到后来精细化的并发包(JUC),Java 在并发控制上的演进,是一部不断在“上下文切换开销”与“数据一致性”之间寻找平衡的历史。
1. synchronized 的涅槃:从“性能杀手”到自适应
早期的 Java 开发中,synchronized 常常被视作性能杀手。因为在 JDK 1.6 之前,它是一个纯粹的重量级锁。每次线程竞争失败,都会直接陷入操作系统级别的阻塞(Mutex Lock),这种从用户态到内核态的频繁切换,其开销往往比执行同步代码本身还要大得多。
但这并不意味着我们要抛弃语言层面的关键字。JVM 团队在后续版本中引入了锁升级机制(无锁 -> 偏向锁 -> 轻量级锁 -> 重量级锁)。
- 偏向锁(Biased Locking): 在绝大多数情况下,锁不仅不存在多线程竞争,甚至总是由同一个线程多次获得。偏向锁通过在对象头中记录线程 ID,让该线程后续连 CAS 操作都不需要就能直接进入同步块。
- 轻量级锁(自旋锁): 当有第二个线程来竞争时,升级为轻量级锁。由于很多同步块的执行时间极短,让等待的线程在 CPU 上“空跑”一会(自旋),反而比挂起线程更高效。
小结: 技术的优化往往遵循“二八定律”。JVM 锁升级的底层哲学是:永远假设最好的情况,并在情况恶化时提供兜底方案。它避免了过早悲观带来的沉重系统开销。
2. 突破 JVM 限制:JUC 的基石 AQS
如果 synchronized 已经足够优化,为什么 Doug Lea 还要在 java.util.concurrent 包中写一套基于 ReentrantLock 的锁机制?
答案是:极致的灵活性与功能扩展。
synchronized 的加锁和释放是隐式的,且不支持中断响应、超时尝试以及公平锁。为了实现这些高级特性,JUC 引入了 AQS(AbstractQueuedSynchronizer)。
AQS 的核心设计极其优雅,它仅仅使用了两样东西就构建了整个并发包的基石:
- 一个 volatile 的
state变量: 用于表示当前的同步状态(例如,被锁定了几次,或者信号量还剩多少)。 - 一个 FIFO 的双向链表(CLH 队列): 用于存储排队等待获取锁的线程。
所有基于 AQS 的同步器(如 ReentrantLock、Semaphore、CountDownLatch),只是在重写 tryAcquire 和 tryRelease 方法,定义自己如何操作这个 state,而繁琐的线程排队、阻塞、唤醒工作,AQS 已经在底层统一处理了。
小结: AQS 的设计是面向对象框架设计的典范。它将“状态管理”开放给子类,将“线程调度”封装在底层,用极简的数据结构解决了复杂的并发协同问题。
3. 锁的尽头是无锁:CAS 与并发哲学
无论是 synchronized 还是 ReentrantLock,本质上依然是悲观锁——“我认为别人会修改数据,所以我先锁上”。而在读多写少的极高并发场景下,悲观锁的排队机制会成为吞吐量的绝对瓶颈。
此时,基于硬件指令支持的 CAS(Compare-And-Swap)成为了突破口。配合 volatile 保证的可见性,Java 提供了 Atomic 系列类和更高级的 LongAdder。它们采用乐观锁的哲学:我不加锁,我只在更新的最后一刻检查数据有没有被别人动过。
当然,CAS 也带来了 ABA 问题和高竞争下的 CPU 自旋消耗(空耗 CPU)。LongAdder 的出现正是为了解决 CAS 的自旋热点问题,它通过将单一的 value 拆分成一个数组(Cell),让多线程把竞争分散到不同的内存块上,最后再求和,完美诠释了“空间换时间”的并发之道。
总结
Java 并发控制的演进,其实是一条逐渐将调度权从操作系统内核态收回到用户态的路径。从依赖 OS 的互斥量,到 JVM 内部的锁升级,再到纯 Java 代码实现的 AQS 队列与 CAS 无锁化设计。在进行架构选型时,没有绝对最好的锁,只有最契合当前业务读写比例、竞争激烈程度的权衡策略。