Java高级面试必问:AQS 到底是什么?
- Java
- 11天前
- 17热度
- 0评论
Java高级面试必问:AQS详解与应用
本文详细介绍了Java并发编程中的核心组件AQS(AbstractQueuedSynchronizer),包括其基本概念、工作原理以及应用场景。
在现代多线程开发中,同步和互斥机制是确保程序正确性和高效性的关键。而在Java平台下,AbstractQueuedSynchronizer(简称AQS) 是一种构建锁和其他同步器的基础框架,提供了强大的并发控制能力,能够实现种类繁多的锁和同步机制。本文将带领读者全面了解AQS的工作原理及其在实际开发中的应用。
一、AQS是什么?
AQS(AbstractQueuedSynchronizer)是一个用于构建高级线程同步机制的类库框架。它为开发者提供了构建自定义锁及同步组件的能力,使得我们不仅能实现标准的互斥锁和读写锁,还能创建出具有特殊功能的并发工具。
二、为什么需要AQS?
1. 问题背景
在Java早期版本中,synchronized关键字是唯一的线程同步机制。然而它存在一些明显的局限性:
- 无法实现公平锁:只能提供非公平的获取策略。
- 缺少超时等待功能:没有支持等待特定时间后放弃锁的功能。
- 缺乏条件变量的支持:无法通过精确控制来唤醒特定线程或组合多个条件表达式。
- 不能中断等待中的线程:一旦进入同步状态,只能通过正常执行流程退出。
2. AQS的解决方案
AQS提供了一个灵活且高效的并发框架。它允许开发者:
- 定义多种自定义锁策略;
- 实现更复杂的同步需求;
- 获得更好的性能优化。
三、AQS的工作原理与架构设计
核心结构:CLH队列 + 状态变量
CLH(Craig, Landin 和 Hagersten)队列 是一种自旋链表,用于维护获取资源的线程顺序。每个节点代表一个等待或持有锁状态线程。
public abstract class AbstractQueuedSynchronizer {
private volatile int state; // 状态变量
private transient volatile Node head; // 队首节点
private transient volatile Node tail; // 队尾节点
static final class Node { // 节点类定义
volatile Node prev;
volatile Node next;
volatile Thread thread;
volatile int waitStatus;
}
}两种同步模式:独占模式与共享模式
独占模式(Exclusive Mode)
- 特点:在一个时刻,仅允许一个线程获取资源。
- 典型应用:ReentrantLock、CountDownLatch
- 核心方法:
- acquire(int arg):尝试获取锁,若无法立即获得,则将当前线程加入等待队列并触发自旋操作。
- release(int arg):释放已经持有的锁,并唤醒下一个节点中的等待线程。
public final void acquire(int arg) {
if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
private Node addWaiter(Node mode) { // 添加到队尾的新节点
Node node = new Node(Thread.currentThread(), mode);
Node pred = tail;
if (pred != null) {
node.prev = pred;
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
enq(node); // 原子添加操作
return node;
}共享模式(Shared Mode)
- 特点:允许多个线程同时获取资源,如读锁或信号量。
- 典型应用:Semaphore、ReentrantReadWriteLock
- 核心方法:
- acquireShared(int arg):尝试获取共享资源。
- releaseShared(int arg):释放持有的共享资源,并可能唤醒多个后继节点。
private void doReleaseShared() {
for (;;) {
Node h = head;
if (h != null && h != tail) { // 检查是否队列中有等待的线程
int ws = h.waitStatus; // 获取头结点状态值
if (ws == Node.SIGNAL) {
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0)) // 修改节点的状态为PROPAGATE,用于传播唤醒信号
continue;
unparkSuccessor(h); // 唤醒下一个等待的线程
}
else if (ws == 0 && compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
while (!compareAndSetWaitStatus(head = h, Node.PROPAGATE, 0)) { } // 继续唤醒后面的节点
}
}
}四、实际开发中如何与AQS交互
场景1:使用现成的AQS实现
在大多数情况下,我们无需直接操作AbstractQueuedSynchronizer类。Java并发库提供了多个基于AQS构建的标准工具类。
import java.util.concurrent.locks.ReentrantLock;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Semaphore;
public class AQSInPractice {
public static void main(String[] args) throws InterruptedException {
// 使用ReentrantLock实现可重入锁,参数true表示启用公平模式
ReentrantLock lock = new ReentrantLock(true);
System.out.println("锁已获取");
lock.unlock();
// 使用CountDownLatch协调多个线程的执行顺序
CountDownLatch latch = new CountDownLatch(3);
for (int i = 0; i < 3; i++) {
new Thread(() -> {
try {
latch.countDown(); // 每次调用减少计数器值
System.out.println("任务" + i + "完成");
} catch (Exception e) {
e.printStackTrace();
}
}).start();
}
latch.await(); // 等待直到所有线程都执行完毕
System.out.println("所有任务完成");
// 使用Semaphore实现信号量控制
}
}通过这些标准的并发工具,可以轻松实现常见的同步需求。希望本文能帮助读者对AQS有一个全面而深入的理解,并在实际开发中灵活运用其强大功能。
下一节我们将继续讨论如何利用AQS自定义锁以及更多高级场景的应用,敬请期待!
场景2:基于AQS实现自定义同步器
可以利用AbstractQueuedSynchronizer (AQS)作为基础,通过继承它来构建满足特定需求的锁机制。以下是一个简单的示例代码,用于创建一个“最多允许N个线程访问”的自定义锁:
public class SimpleLimitLock {
private final Sync sync;
public SimpleLimitLock(int limit) {
sync = new Sync(limit);
}
private static class Sync extends AbstractQueuedSynchronizer {
Sync(int limit) {
setState(limit); // 将初始状态设置为允许的最大线程数
}
protected int tryAcquireShared(int acquires) {
int available = getState();
int remaining = available - acquires;
if (remaining < 0 || compareAndSetState(available, remaining)) {
return remaining; // 如果剩余许可不足,返回负值;如果CAS成功,则返回剩余许可数
}
return -1; // 表示尝试获取失败
}
protected boolean tryReleaseShared(int releases) {
int current = getState();
int next = current + releases;
if (compareAndSetState(current, next)) {
return true; // CAS更新状态成功
}
return false; // 返回CAS操作的状态结果
}
}
public void lock() { sync.acquireShared(1); }
public void unlock() { sync.releaseShared(1); }
}这段代码展示了如何通过AQS来控制线程访问资源的数量,确保并发时的线程安全。此类自定义同步器适用于需要精细控制访问权限的应用场景。
五、AQS在Java并发体系中的角色
AbstractQueuedSynchronizer (AQS) 在 Java 并发编程中扮演着至关重要的角色。它提供了构建高级同步对象的基础结构,如锁和信号量等。下图展示了 Java 并发编程的层次结构:
// Java并发编程的层次结构:
┌─────────────────────────────────────────────────┐
│ Java并发应用层 │
│ • 线程池 (ExecutorService) │
│ • 并发集合 (ConcurrentHashMap) │
│ • 异步任务 (CompletableFuture) │
├─────────────────────────────────────────────────┤
│ AQS工具类层 │
│ • ReentrantLock │
│ • Semaphore │
│ • CountDownLatch │
│ • CyclicBarrier │
├─────────────────────────────────────────────────┤
│ AQS框架层 │
│ • 状态管理 (state) │
│ • 队列管理 (CLH队列) │
│ • 线程阻塞/唤醒 (LockSupport) │
├─────────────────────────────────────────────────┤
│ JVM/OS层 │
│ • synchronized监视器锁 │
│ • CAS指令 (Unsafe类) │
│ • 线程调度 (操作系统) │
└─────────────────────────────────────────────────┘六、AQS的工作流程示例
以 ReentrantLock 的加锁过程为例,其工作原理主要依赖于 AQS 提供的模板方法和内部状态管理机制。具体步骤如下:
sync.acquire(1); // 调用AQS的模板方法
abstract static class Sync extends AbstractQueuedSynchronizer {
// AQS.acquire() 的调用链:
// 1. tryAcquire() 尝试获取锁(子类实现)
// 2. addWaiter() 创建节点加入队列
// 3. acquireQueued() 在队列中自旋/阻塞
// 4. 如果被中断过,自我中断恢复状态
}整个流程确保了线程安全及资源的正确释放。例如,在尝试获取锁失败的情况下,线程会被放置在等待队列中直至获得锁;若在此期间发生中断请求,则会适当处理并恢复相关状态。
七、实际开发中的最佳实践
1. 选择合适的同步工具
根据具体场景的需求来挑选最适用的并发控制工具。例如:
public class SyncToolSelection {
void useReentrantLock() {
ReentrantLock lock = new ReentrantLock(true); // 公平锁,适用于需要公平获取锁的应用程序
}
void useSemaphore() {
Semaphore semaphore = new Semaphore(10); // 最多允许10个线程同时访问
// 适用于数据库连接池、限流场景等资源受限的环境
}
void useCountDownLatch() {
CountDownLatch latch = new CountDownLatch(5);
// 适用于启动阶段等待初始化完成,确保所有部件就绪后进入工作状态
}
void useCyclicBarrier() {
CyclicBarrier barrier = new CyclicBarrier(4);
// 用于并行计算场景中,多个线程需同步到达某个固定点
}
}2. 避免常见陷阱
在使用AQS相关类时需要注意以下问题:
public class AQSPitfalls {
ReentrantLock lock = new ReentrantLock();
void conditionVariableMistake() {
Condition condition = lock.newCondition(); // 错误示例:没有在lock保护下调用await
try {
lock.lock();
if (someCondition) {
return; // 忘记解锁!
}
lock.unlock();
condition.await(); // 正确的调用方式,必须在持有锁的状态下进行
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}八、总结:AQS在并发编程中的角色
- 框架提供者 :为构建高级同步对象(如锁和信号量)提供了通用的实现框架。
- 基础设施 :作为Java并发包的核心机制,如同建造大楼的地基一样重要。
- 性能优化 :相比synchronized关键字提供了更灵活且高效的线程控制方式。
- 功能扩展 :支持各种高级特性,如公平/非公平模式、可重入性以及读写锁分离等。
> 🔗 相关阅读:并发编程