跳转至

AQS

AbstractQueuedSynchronizer

为什么需要 AQS

AQS 在 ReentrantLock、ReentrantReadWriteLock、Semaphore、CountDownLatch、ThreadPoolExcutor 的 Worker 中都有运用(JDK 1.8)

以及 ReentrantLock 和 ReentrantReadWriteLock 中的公平锁和非公平锁的机制

  • Semaphore 维护同时最多有多少线程可以访问公共资源
  • Reentrant 维护最多只能那个有一个线程访问公共线程

AQS 负责:

  1. 状态的原子性管理
  2. 线程的阻塞与解除阻塞
  3. 队列的管理

AQS 是一个用于构建锁、同步器等线程协作工具类的框架,有了 AQS 以后,很多用于线程协作的工具类就都可以很方便的被写出来,有了 AQS 之后,可以让更上层的开发极大的减少工作量,避免重复造轮子,同时也避免了上层因处理不当而导致的线程安全问题。

AQS 的内部原理

1. state 属性

private volatile int state;

在 AQS 中有 state 这样的一个属性,是被 volatile 修饰的,会被多个线程并发修改,它代表当前工具类的某种状态,在不同的类中代表不同的含义。

  • Semaphore:表示剩余的许可证的数量
  • CountDownLatch:表示还剩的需要倒数的数量,每次调用 countDown 方法会减一
  • ReentrantLock:表示锁的占有情况,最开始是 0 ,表示没有线程占有锁;被线程持有并重入后 state 就会逐步累加

两个与 state 相关的方法:

  1. protected final boolean compareAndSetState(int expect, int update)

    使用 Unsafe 里面的 CAS 操作修改变量(也会用来判断是否需要入队)

  2. protected final void setState(int newState)

    直接赋值,这是因为对基本类型的变量进行赋值(或者直接修改引用的对象)时,加上 volatile 可以保证线程安全

2. CLH 队列(CLH Lock Queue)

双向链表,head 表示当前持有锁的线程(独占式 exclusive 锁是这样,当然也可能只是一个占位符);在头节点之后的线程就被阻塞了,它们会等待 AQS 负责的唤醒操作。

获取同步状态失败的线程将会被包装为一个节点放入同步队列的尾部,并在队列中自旋(并不是简单的 busy waiting,而是可能会在等待时间较长后被阻塞 park(),直到被唤醒 unpark())

  • 独占式
  • 共享式
  • 超时式(实现了传统 synchronied 关键字不具备的特性)

独占式获取同步状态的流程:

image-20250218202231497

  • 非公平锁在获取锁时,直接尝试通过 CAS 操作修改锁的状态,而不检查队列中是否有等待的线程。
  • 公平锁在获取锁时,首先检查队列中是否有等待的线程。如果有等待的线程,则当前线程会进入队列等待。

3. 获取 / 释放方法

在 AQS 中除了刚才讲过的 state 和队列之外,还有一部分非常重要,那就是获取和释放相关的重要方法,这些方法是协作工具类的逻辑具体体现,需要每一个协作工具类自己去实现,所以在不同的工具类中,它们的实现和含义各不相同。

获取方法 tryAquire()

获取操作会依赖于 state 变量的值,获取的时候经常会阻塞:

  • ReentrantLock 中的 lock 方法就是其中一个“获取方法”,执行时,如果发现 state 不等于 0 且当前线程不是持有锁的线程,那么就代表这个锁已经被其他线程所持有了。这个时候,当然就获取不到锁,于是就让该线程进入阻塞状态。
  • Semaphore 中的 acquire 方法就是其中一个“获取方法”,作用是获取许可证,此时能不能获取到这个许可证也取决于 state 的值。如果 state 值是正数,那么代表还有剩余的许可证,数量足够的话,就可以成功获取;但如果 state 是 0,则代表已经没有更多的空余许可证了,此时这个线程就获取不到许可证,会进入阻塞状态,所以这里同样也是和 state 的值相关的。
  • CountDownLatch 获取方法就是 await 方法(包含重载方法),作用是“等待,直到倒数结束”。执行 await 的时候会判断 state 的值,如果 state 不等于 0,线程就陷入阻塞状态,直到其他线程执行倒数方法把 state 减为 0,此时就代表现在这个门闩放开了,所以之前阻塞的线程就会被唤醒。

例如公平锁的 acquire

protected boolean tryAcquire(int acquires) {
    if (hasQueuedPredecessors()) {
        return false;
    }
    if (compareAndSetState(0, 1)) { // 根据 state
        setExclusiveOwnerThread(Thread.currentThread());
        return true;
    }
    return false;
}

释放方法 tryRelease()

释放方法是站在获取方法的对立面的,通常和刚才的获取方法配合使用。获取方法可能会让线程阻塞,比如说获取不到锁就会让线程进入阻塞状态,但是释放方法通常是不会阻塞线程的。

比如在 Semaphore 信号量里面,释放就是 release 方法(包含重载方法),release() 方法的作用是去释放一个许可证,会让 state 加 1;而在 CountDownLatch 里面,释放就是 countDown 方法,作用是倒数一个数,让 state 减 1。所以也可以看出,在不同的实现类里面,他们对于 state 的操作是截然不同的,需要由每一个协作类根据自己的逻辑去具体实现。

如何使用 AQS

同步器的主要使用方式是继承,子类通过继承同步器重写方法来管理同步状态,一般使用同步器提供的三个方法来进行操作:

  • getState()
  • setState()
  • compareAndSetState()

子类推荐被定义为 自定义同步组件 的静态内部类

(有 5 个可重写方法 / 9 个模板方法)