前言
AQS(AbstractQueuedSynchronizer)是 Java 并发包(java.util.concurrent)中的核心基础类,它提供了一个框架来实现阻塞锁和相关的同步器(如信号量、CountDownLatch 等)。AQS 内部使用了一个 FIFO 的双向队列来管理线程,这个队列存储的是等待获取同步状态的线程节点。
一、AQS 的核心原理
1. AQS 的作用是什么?
AQS 是一个用于构建锁和同步器的框架,它通过 FIFO 双向队列 管理等待线程,并通过 volatile int state
变量实现资源状态的原子操作。开发者只需重写少量方法,即可自定义同步工具。
2. AQS 的底层结构
(1) 资源状态(state
)
操作方式:
getState()
:获取当前状态值。setState(int newState)
:直接设置状态值。compareAndSetState(int expect, int update)
:通过 CAS 原子更新状态。
实际应用:
ReentrantLock
:state
表示锁的重入次数(0=未锁定,1=已锁定,>1=重入)。Semaphore
:state
表示剩余的许可证数量。
(2) 等待队列(CLH 变种)
- 双向链表实现,每个节点(
Node
)封装一个等待线程。 节点状态:
CANCELLED
(线程已取消等待)SIGNAL
(后继节点需要被唤醒)CONDITION
(线程在条件队列中等待)
3. AQS 的两种模式
(1) 独占模式(Exclusive)
- 特点:同一时间只有一个线程能获取资源。
- 应用场景:
ReentrantLock
、ReentrantReadWriteLock.WriteLock
。 核心方法:
acquire(int arg)
:获取资源,失败则进入队列阻塞。release(int arg)
:释放资源,唤醒后继节点。
(2) 共享模式(Shared)
- 特点:多个线程可同时获取资源。
- 应用场景:
Semaphore
、CountDownLatch
。 核心方法:
acquireShared(int arg)
:共享式获取资源。releaseShared(int arg)
:共享式释放资源,传播唤醒信号。
4. 如何自定义同步器?
继承 AbstractQueuedSynchronizer
并重写以下方法:
// 独占模式
protected boolean tryAcquire(int arg) { ... } // 尝试获取资源
protected boolean tryRelease(int arg) { ... } // 尝试释放资源
// 共享模式
protected int tryAcquireShared(int arg) { ... }
protected boolean tryReleaseShared(int arg) { ... }
// 是否被当前线程独占
protected boolean isHeldExclusively() { ... }
二、AQS 高频面试题
1. 公平锁与非公平锁的区别?AQS 如何实现?
非公平锁:线程直接尝试 CAS 抢占资源,无需检查等待队列。
优点:减少线程切换,吞吐量高。
缺点:可能导致线程饥饿。
公平锁:线程抢占资源前,先调用 hasQueuedPredecessors() 检查队列中是否有等待线程。
优点:保证先到先得,避免饥饿。
缺点:上下文切换频繁,吞吐量较低。
源码对比:
// 非公平锁(ReentrantLock.NonfairSync)
protected final boolean tryAcquire(int acquires) {
if (compareAndSetState(0, 1)) { ... } // 直接 CAS 抢锁
}
// 公平锁(ReentrantLock.FairSync)
protected final boolean tryAcquire(int acquires) {
if (hasQueuedPredecessors()) return false; // 先检查队列
if (compareAndSetState(0, 1)) { ... }
}
2. 为什么 AQS 使用双向队列而非单向队列?
线程取消的高效处理:当线程因超时或中断取消等待时,需要快速修改前驱和后继节点的指针。
时间复杂度优化:
单向队列删除中间节点需遍历,时间复杂度 O(n)。
双向队列通过 prev 和 next 指针直接操作,时间复杂度 O(1)。
3. AQS 的 ConditionObject 和 Object.wait() 有什么区别?
对比维度 | ConditionObject | Object.wait() |
---|---|---|
绑定对象 | 必须绑定到 AQS 实现的锁(如 ReentrantLock) | 任意 synchronized 对象 |
多条件队列 | 支持多个条件队列(如生产者-消费者模型分组) | 仅一个等待队列 |
唤醒精度 | signal() 精确唤醒指定条件队列的线程 | notify() 随机唤醒一个线程 |
中断支持 | 提供 awaitUninterruptibly() 方法 | 需手动处理中断 |
4. AQS 如何处理线程中断?
不可中断模式(默认):*
线程调用 acquire() 时被中断,会继续等待直到获取资源,再补上中断标记。
可中断模式:*
线程调用 acquireInterruptibly() 时被中断,直接抛出 InterruptedException。
5. 共享模式下的唤醒传播是什么?
共享模式(如 Semaphore)释放资源时,会唤醒队列中的第一个等待节点,
并由该节点继续唤醒后续共享节点,形成“连锁反应”,直到无可用资源或队列为空。
优势:减少唤醒次数,提升并发效率。
6. AQS 中的同步状态(state)有什么作用?
答:同步状态(state)用于表示同步器的状态,不同的同步器对 state 的含义和操作方式不同。
例如在 ReentrantLock 中表示锁的重入次数,在 Semaphore 中表示可用的许可证数量。
7. AQS 队列的作用是什么?
答:AQS 队列用于存储等待获取同步状态的线程节点,当线程获取同步状态失败时,会被加入到队列中等待唤醒。
8. 说说 AQS 中 acquire 和 acquireInterruptibly 方法的区别?
答:acquire 方法是独占式获取同步状态,如果获取失败则将当前线程加入等待队列并阻塞,且不响应中断;
acquireInterruptibly也是独占式获取同步状态,但允许线程被中断,
当线程被中断时会抛出 InterruptedException。
9. 如何基于 AQS 实现一个简单的自定义同步器?
答:首先继承 AQS 类,然后实现 tryAcquire、tryRelease(独占模式)或
tryAcquireShared、tryReleaseShared(共享模式)等方法来定义同步状态的获取和释放逻辑。
例如,要实现一个简单的独占锁,可以如下实现:
import java.util.concurrent.locks.AbstractQueuedSynchronizer;
class MyLock {
private static class Sync extends AbstractQueuedSynchronizer {
@Override
protected boolean tryAcquire(int arg) {
// 尝试获取锁,只有当 state 为 0 时才能获取成功
if (compareAndSetState(0, 1)) {
setExclusiveOwnerThread(Thread.currentThread());
return true;
}
return false;
}
@Override
protected boolean tryRelease(int arg) {
// 释放锁,将 state 设为 0
if (getState() == 0) {
throw new IllegalMonitorStateException();
}
setExclusiveOwnerThread(null);
setState(0);
return true;
}
}
private final Sync sync = new Sync();
public void lock() {
sync.acquire(1);
}
public void unlock() {
sync.release(1);
}
}
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。