从 synchronized 到 AQS 的演进
在 Java 并发编程中,线程间的同步与互斥主要依赖两大底层体系:JVM 内置的 synchronized 关键字体系,以及基于 Java 代码实现的 AQS(AbstractQueuedSynchronizer)体系。虽然它们在 Java 层的表现差异巨大,但在底层最终都依赖于操作系统的互斥与同步原语。本文将深度剖析这两大机制的底层原理、演进过程及适用场景。
一、 synchronized 的底层原理(重量级锁机制)
synchronized 的底层强依赖于 JVM 的 C++ 实现。当竞争激烈膨胀为“重量级锁”时,其核心依赖于对象头指向的 ObjectMonitor(管程)对象。
1. ObjectMonitor 核心数据结构
ObjectMonitor 是实现互斥与条件等待的核心组件:
_owner:指向当前持有锁的 Java 线程指针。为空表示锁空闲。_recursions:记录当前锁被同一线程重入的次数。_EntryList:阻塞队列。多线程竞争锁失败时,被封装为ObjectWaiter节点加入此队列,线程进入BLOCKED状态。_WaitSet:等待队列。持有锁的线程调用wait()后,交出锁并进入此队列,线程进入WAITING状态。
2. 加锁与释放流程
加锁(monitorenter):线程通过 CAS 尝试将 _owner 设为自己。成功则获取锁;若 _owner 已是自己则 _recursions 累加(重入);否则,进入 _EntryList 阻塞排队。
释放(monitorexit):先检查 _owner是否是自己,如果是,_recursions 减 1。减至 0 时清空 _owner。随后根据特定策略(通常从 _EntryList 头部)挑选一个线程唤醒,参与下一轮的 CAS 抢锁竞争。
3. OS 层面的重量级操作
在 Linux 系统下,ObjectMonitor 的阻塞与唤醒直接调用了 POSIX 线程库(pthreads):
- 阻塞:调用
pthread_mutex_lock()。若拿不到锁,最终触发 Linux 的futex_wait系统调用,当前线程陷入内核态睡眠。 - 唤醒:调用
pthread_cond_signal(),操作系统收到信号,唤醒等待队列中的某一个线程。
局限性分析:synchronized 的“重”在于它将排队逻辑全交给了底层操作系统。大量竞争线程频繁触发 pthread_mutex 操作,导致极高频的用户态与内核态切换。(AQS排队的逻辑上浮到 Java 层中进行)
二、 AQS 的底层原理
1. 核心状态管理:volatile int state
AQS 内部维护了一个 32 位的整型变量,用于表示当前的同步状态:private volatile int state;
- 内存可见性:通过
volatile关键字修饰,保证了多线程环境下的变量可见性,任何一个线程对state的修改都能被其他线程立刻感知。 - 原子性操作:AQS 提供了
getState()、setState()和compareAndSetState(expect, update)方法。状态的竞争变更强依赖于底层Unsafe类的 CAS (Compare-And-Swap) 操作,这确保了在高并发下的状态转移是绝对原子的。 - 语义抽象:AQS 本身并不定义
state的具体业务含义。它仅仅将state抽象为一个资源标识,交由子类去定义。例如,它可以是重入次数、剩余许可证数量,或者是读写锁的高低位。
2. 同步队列底座:变体 CLH 自旋等待队列
当线程尝试获取同步状态失败时,AQS 需要一种机制来管理这些被阻塞的线程。它采用的是一个基于链表的自旋等待队列(CLH 队列的变体)。
- 双向链表结构:AQS 的队列是一个 FIFO(先进先出)的虚拟双向队列。队列的每个节点(
Node)封装了当前等待的线程引用、节点的等待状态(waitStatus)、前驱节点(prev)和后继节点(next)。 - 尾部入队,头部出队:新加入等待的线程会通过 CAS 操作安全地挂接到队列的尾部(
tail);而当前持有锁的线程在释放资源时,会负责唤醒其后继节点(即head的下一个节点)。 - waitStatus 的重要性:节点的状态决定了线程的行为,例如
SIGNAL (-1)表示后继节点需要被唤醒,CANCELLED (1)表示线程已取消等待。
3. 线程调度引擎:LockSupport
在节点入队后,如果前驱节点不是头节点或者尝试获取锁依然失败,线程需要被真正地挂起,避免通过死循环(纯自旋)白白消耗 CPU 资源。AQS 依赖 LockSupport 工具类来实现这一操作。
- 底层机制:
LockSupport的底层通过操作系统的Mutex(互斥锁)和Condition Variable(条件变量)来实现线程的阻塞(park)与唤醒(unpark)。 - 优势:相比于传统的
Object.wait()和notify(),LockSupport.park()不需要提前获取对象级别的监视器锁(Monitor),且unpark可以在park之前调用(发放许可),从根本上避免了传统线程同步机制中容易引发的死锁问题。
4. 模板方法模式
AQS 封装了复杂的排队(CLH 链表自旋队列)、阻塞与唤醒逻辑。子类只需重写 tryAcquire/tryRelease(独占模式)或 tryAcquireShared/tryReleaseShared(共享模式),即可定义锁的具体语义。
AQS 衍生工具的实现逻辑:
| 并发工具 | AQS 模式 | state 核心业务语义 |
tryAcquire 成功条件 |
|---|---|---|---|
| ReentrantLock | 独占 | 锁状态(0为开,>0为关)及重入次数 | state == 0 通过 CAS 改为1,或当前线程已持有。 |
| Semaphore | 共享 | 剩余可用许可证总数量 | state - acquires >= 0。 |
| CountDownLatch | 共享 | 还需要等待触发的事件次数 | state == 0。 |
| ReadWriteLock | 混合 | 拆分32位:高16位为读锁次数,低16位为写重入次数 | 读锁:无他人写锁。写锁:无读锁且无他人写锁。 |
5. Condition 机制(多条件协作)
AQS 配合 Condition 接口,支持在同一把锁上创建多个条件等待队列。调用 await() 的线程进入指定的 Condition 单向队列;调用 signal() 时,AQS 精准地将节点从条件队列移至 CLH 队列,实现了精准定向唤醒,彻底解决了 synchronized 中 notifyAll 引发的“惊群效应”。
三、锁实现中的线程状态流转解析
1. synchronized 的状态流转
- 线程尝试获取锁,若
_owner非空,进入_EntryList,状态由RUNNABLE转换为BLOCKED。 - 持有锁的线程调用
wait(),交出锁,进入_WaitSet,状态由RUNNABLE转换为WAITING。 - 接收到
notify()后,线程从_WaitSet移至_EntryList,状态由WAITING转换为BLOCKED。 - 最终再次抢到锁时,状态由
BLOCKED恢复为RUNNABLE。
2. AQS 锁(如 ReentrantLock)的状态流转
AQS 体系中,排队等待锁的线程绝不会进入 BLOCKED 状态,BLOCKED 是 synchronized 监视器底层的专属状态。
- 线程调用
tryAcquire失败后,AQS 将其加入 CLH 队列,并调用LockSupport.park(),状态由RUNNABLE转换为WAITING。 - 线程调用
Condition.await()时,加入条件队列并调用park(),状态由RUNNABLE转换为WAITING。 - 接收到
Condition.signal()后,AQS 仅将节点从条件队列移至 CLH 队列,线程依旧处于park挂起状态,状态维持WAITING不变。 - 当前驱节点释放锁并调用
unpark()唤醒后,线程从park()返回,状态由WAITING直接转换为RUNNABLE进行 CAS 抢锁。
AQS 线程一直处于 WAITING 状态,意味着可以别的线程通过 interrupt 方法打破,至于AQS线程被打断之后如何处置,取决于调用的方法是否有“中断”;
五、 总结与适用场景
为什么引入 AQS 与 LockSupport 机制?
既然 Java 已经提供了基于底层对象监视器(ObjectMonitor)的 synchronized 关键字来实现加锁,为什么还需要在 JDK 1.5 中引入 AQS 框架以及底层的 LockSupport 机制?原因在于 synchronized 机制在应对高并发与复杂业务场景时存在固有局限性:
- 不可中断的阻塞等待:当线程尝试获取
synchronized锁失败而进入排队队列时,其状态变为BLOCKED。处于该状态的线程对Thread.interrupt()中断信号不做出响应操作,必须一直等待获取锁。这在处理死锁或需快速失败的场景中缺乏灵活性。AQS 基于LockSupport.park可以响应中断并随时退出等待。 - 缺乏非阻塞获取与超时机制:
synchronized只能进行阻塞式获取。AQS 提供了tryLock()(非阻塞尝试)以及tryLock(long timeout, TimeUnit unit)(超时自动返回)的能力。 - 缺乏公平锁机制:
synchronized的唤醒策略依赖底层操作系统的调度,属于非公平锁,可能导致某些线程长期获取不到执行权(线程饥饿)。AQS 可通过内部队列实现严格的 FIFO 公平调度。 - 单一的条件等待队列:一个
synchronized锁只能关联一个wait()/notify()的等待队列。AQS 配合Condition接口,可以在同一把锁上创建多个条件等待队列,实现精准的定向唤醒,避免“惊群效应”。
| 维度 | synchronized 体系 |
AQS (ReentrantLock 等) |
|---|---|---|
| 操作灵活性 | 隐式加解锁,不可中断死等 | 显式加解锁,支持响应中断、超时放弃 |
| 队列与唤醒 | C++ 层队列,随机唤醒,单条件队列 | Java 层 CLH 队列,精准唤醒,多条件队列 |
| 适用场景 | 逻辑相对简单、锁竞争不激烈、无需复杂中断处理的常规同步模块。 | 需要极高吞吐量的底层并发组件设计(线程池),及复杂锁路由的高并发业务模块。 |
更多推荐



所有评论(0)