AQS核心原理
AQS 核心原理
AQS(AbstractQueuedSynchronizer) 是 Java 并发包的核心基石,ReentrantLock、CountDownLatch、Semaphore 等锁 / 同步工具底层都基于 AQS 实现。
它的核心设计:用一个 state 变量控制同步状态 + 一个 CLH 双向队列管理等待线程,通过 CAS 实现无锁竞争,高效实现线程阻塞 / 唤醒。
一、AQS 核心结构
AQS 最核心的就两个东西:状态变量 + 等待队列
1 | |
1. state 同步状态
- volatile 修饰:保证多线程可见性
- 含义由实现类定义:
- ReentrantLock:
0=无锁,1=持有锁,>1=重入次数 - Semaphore:剩余可用许可数
- CountDownLatch:剩余需要倒数的次数
- ReentrantLock:
- 修改变量必须用 CAS(无锁原子操作),保证线程安全。
2. CLH 等待队列
- 双向链表:每个节点封装一个等待线程
- 节点核心属性:
thread:绑定的等待线程waitStatus:节点状态(0 = 初始化,1 = 取消,-1 = 需要唤醒后继)prev/next:前驱 / 后继指针
- 作用:没抢到锁的线程,会被封装成节点加入队列,排队等待唤醒。
二、独占模式 vs 共享模式
AQS 支持两种同步模式,是锁 / 工具的核心区别:
1. 独占模式(Exclusive)
- 同一时间只有一个线程能持有锁
- 典型实现:
ReentrantLock - 核心方法:
tryAcquire()(抢锁)、tryRelease()(释放锁)
2. 共享模式(Shared)
- 多个线程可以同时持有锁 / 许可
- 典型实现:
Semaphore、CountDownLatch、ReentrantReadWriteLock读锁 - 核心方法:
tryAcquireShared()(抢共享资源)、tryReleaseShared()(释放共享资源)
三、核心逻辑:入队、出队、抢锁
独占模式
1. 抢锁逻辑
- 线程调用
lock(),尝试CAS 修改 state:- 成功 → 直接持有锁,设置锁持有者为当前线程
- 失败 → 进入入队流程
- 抢锁是自旋 + CAS,避免频繁阻塞线程(提升性能)。
2. 入队逻辑
抢锁失败的线程,会被封装成 Node 加入 CLH 队列尾部:
- 用CAS 安全添加到队尾(多线程并发入队无锁)
- 入队后,阻塞当前线程(
LockSupport.park()),让出 CPU - 队列是先进先出(FIFO),保证公平性(非公平锁会先插队再入队)
头节点是哨兵节点(不存线程),真正抢锁的是头节点的后继节点。
3. 出队逻辑
持有锁的线程释放锁时:
- CAS 修改
state为 0 - 唤醒头节点的后继节点(
LockSupport.unpark()) - 被唤醒的线程再次尝试抢锁:
- 成功 → 成为新的头节点,原头节点出队
- 失败 → 重新阻塞
共享模式
1. 抢锁逻辑
线程调用共享锁获取方法,尝试 CAS 修改 state(获取共享资源):
tryAcquireShared 返回 >0 → 获取成功,还有剩余资源,需要向后继节点传播唤醒
tryAcquireShared 返回 =0 → 获取成功,无剩余资源,不传播唤醒
tryAcquireShared 返回 <0 → 获取失败,进入入队流程
抢锁是自旋 + CAS,避免频繁阻塞线程(提升性能)。
2. 入队逻辑
抢共享资源失败的线程,会被封装成 SHARED 类型 Node 加入 CLH 队列尾部:
用 CAS 安全添加到队尾(多线程并发入队无锁)
入队后,阻塞当前线程(LockSupport.park ()),让出 CPU
队列是先进先出(FIFO),头节点是哨兵节点(不存线程),真正抢资源的是头节点的后继节点。
3. 出队 + 传播唤醒逻辑
持有共享资源的线程释放锁时:
CAS 修改 state,归还共享资源
唤醒头节点的后继节点(LockSupport.unpark ())
被唤醒的线程再次尝试抢共享资源:
成功 → 成为新的头节点,原头节点出队,根据资源剩余情况继续唤醒下一个共享节点(传播唤醒)
失败 → 重新阻塞
直到资源耗尽或遇到独占节点,停止唤醒。
四、高级机制:可中断、超时等待
AQS 内置了灵活的线程控制机制,是 Java 锁比 synchronized 更强大的原因:
1. 可中断机制
- 线程在队列等待时,可以响应中断,不会一直死等
- 方法:
lockInterruptibly() - 逻辑:中断信号会唤醒线程,直接抛出
InterruptedException,退出等待
2. 超时等待机制
- 线程只等待指定时间,超时未抢到锁就返回失败
- 方法:
tryLock(long timeout, TimeUnit unit) - 逻辑:
- 线程挂起时记录超时时间
- 时间到自动唤醒,尝试最后一次抢锁
- 仍失败 → 退出队列,返回 false
五、极简流程总结
独占模式
- 线程 A 抢锁 → CAS 修改 state=0→1 → 成功,持有锁
- 线程 B 抢锁 → CAS 失败 → 封装节点入队阻塞
- 线程 C 抢锁 → CAS 失败 → 封装节点入队阻塞
- 线程 A 释放锁 → state=0 → 唤醒队列第一个等待线程
- 线程 B 被唤醒 → 重新抢锁 → 成功,成为新头节点
共享模式
以 Semaphore 3 个许可、5 个线程竞争为例:
- 线程 1/2/3 直接 CAS 抢 state 成功,直接运行;
- 线程 4/5 抢资源失败,封装为 SHARED 节点,追加到 CLH 队列阻塞;
- 线程 1 执行完毕,
releaseShared归还许可,state +1; - 唤醒队列第一个等待线程 4,线程 4 抢锁成功;
- 因为还有剩余许可,自动传播唤醒,继续唤醒线程 5;
- 无剩余资源后,停止唤醒,后续线程继续排队。