面试基础---深入解析 AQS

发布于:2025-02-27 ⋅ 阅读:(13) ⋅ 点赞:(0)

深入解析 AQS:从源码到实践,剖析 ReentrantLock 和 Semaphore 的实现

引言

在 Java 并发编程中,AbstractQueuedSynchronizer(AQS)是一个核心框架,它为构建锁和其他同步器提供了基础支持。ReentrantLockSemaphore 是 AQS 的两个典型实现,分别用于实现可重入锁和信号量。本文将从底层源码的角度,深入分析 AQS 的核心机制,并结合 ReentrantLockSemaphore 的实际应用场景,探讨其设计思想与实现细节。


一、AQS 的核心机制

1.1 AQS 的设计思想

AQS 的核心思想是通过一个 FIFO 队列(CLH 队列)来管理线程的排队和唤醒机制,同时结合一个 state 变量来表示同步状态。AQS 提供了两种模式:

  • 独占模式:同一时刻只有一个线程可以获取资源(如 ReentrantLock)。
  • 共享模式:多个线程可以同时获取资源(如 Semaphore)。

1.2 核心数据结构

1.2.1 state 变量

state 是 AQS 的核心变量,用于表示同步状态。例如:

  • ReentrantLock 中,state 表示锁的重入次数。
  • Semaphore 中,state 表示剩余的许可数。
private volatile int state; // 同步状态
1.2.2 CLH 队列

AQS 使用 CLH 队列(Craig, Landin, and Hagersten 锁队列)来管理等待线程。每个线程被封装为一个 Node 节点,节点中保存了线程的状态(如等待、取消)以及前驱和后继节点的引用。

static final class Node {
    volatile int waitStatus; // 线程状态
    volatile Node prev;      // 前驱节点
    volatile Node next;      // 后继节点
    volatile Thread thread;  // 等待线程
}

1.3 关键方法

1.3.1 acquire(int arg)

acquire 是获取资源的核心方法。它的主要逻辑如下:

  1. 调用 tryAcquire 尝试获取资源。
  2. 如果获取失败,将当前线程封装为 Node 并加入 CLH 队列。
  3. 通过自旋和 LockSupport.park() 挂起线程,等待唤醒。
public final void acquire(int arg) {
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}
1.3.2 release(int arg)

release 是释放资源的核心方法。它的主要逻辑如下:

  1. 调用 tryRelease 尝试释放资源。
  2. 如果释放成功,唤醒 CLH 队列中的下一个线程。
public final boolean release(int arg) {
    if (tryRelease(arg)) {
        Node h = head;
        if (h != null && h.waitStatus != 0)
            unparkSuccessor(h);
        return true;
    }
    return false;
}

二、ReentrantLock 的实现

2.1 公平锁与非公平锁

ReentrantLock 支持公平锁和非公平锁两种模式:

  • 公平锁:严格按照 CLH 队列的顺序获取锁。
  • 非公平锁:允许插队,新线程可以直接尝试获取锁。
2.1.1 公平锁的实现

公平锁在 tryAcquire 中会检查是否有前驱节点,如果有则放弃获取锁。

protected final boolean tryAcquire(int acquires) {
    if (getState() == 0 && !hasQueuedPredecessors() &&
        compareAndSetState(0, acquires)) {
        setExclusiveOwnerThread(currentThread());
        return true;
    }
    return false;
}
2.1.2 非公平锁的实现

非公平锁在 tryAcquire 中不会检查前驱节点,直接尝试获取锁。

final boolean nonfairTryAcquire(int acquires) {
    if (getState() == 0 && compareAndSetState(0, acquires)) {
        setExclusiveOwnerThread(currentThread());
        return true;
    }
    return false;
}

2.2 tryLock()lock() 方法

  • lock():调用 acquire(1),如果获取失败则进入等待队列。
  • tryLock():调用 tryAcquire(1),立即返回是否获取成功。

三、Semaphore 的实现

3.1 信号量的许可数管理

Semaphore 通过 state 变量表示剩余的许可数。acquire()release() 方法分别用于获取和释放许可。

3.1.1 acquire()

acquire 方法会减少 state 的值,如果许可不足则进入等待队列。

public void acquire() throws InterruptedException {
    sync.acquireSharedInterruptibly(1);
}
3.1.2 release()

release 方法会增加 state 的值,并唤醒等待线程。

public void release() {
    sync.releaseShared(1);
}

四、实际应用场景

4.1 ReentrantLock 的应用

  • 高并发环境下的资源竞争控制:如数据库连接池的并发访问。
  • 可重入特性:支持同一线程多次获取锁,避免死锁。

4.2 Semaphore 的应用

  • 线程池的任务调度:通过信号量限制并发任务数。
  • 限流:控制系统的并发请求数,防止资源耗尽。

五、性能优化和注意事项

5.1 性能优化

  • 自旋锁:在短时间内通过自旋尝试获取锁,减少线程切换的开销。
  • CAS 操作:通过 compareAndSetState 实现无锁化的状态更新。

5.2 注意事项

  • 避免死锁:确保锁的获取和释放成对出现。
  • 合理设置超时:使用 tryLock(long timeout, TimeUnit unit) 避免长时间等待。

六、总结与展望

AQS 是 Java 并发包的基石,其设计思想(CLH 队列 + state 变量)为构建高效、灵活的同步器提供了强大支持。ReentrantLockSemaphore 是 AQS 的典型应用,分别解决了独占资源和共享资源的同步问题。

未来,AQS 可能会在以下方面进一步优化:

  1. 更高效的自旋策略。
  2. 对 NUMA 架构的更好支持。
  3. 更灵活的同步模式扩展。

附录

图表:CLH 队列结构

Head -> Node1 -> Node2 -> Node3 -> Tail

在这里插入图片描述

关键代码片段

  • AQS 的 acquirerelease 方法。
  • ReentrantLock 的公平锁与非公平锁实现。
  • Semaphoreacquirerelease 方法。

通过本文的分析,相信读者能够深入理解 AQS 的设计思想及其在 ReentrantLockSemaphore 中的应用,为高并发编程打下坚实基础。