AQS,全称 AbstractQueuedSynchronizer,中文直译过来就是 “抽象的队列式的同步器”,它定义了多线程访问共享资源的同步器框架,J.U.C 包中很多同步类实现都依赖于它,比如我们常用的锁:ReentrantLock,又比如一些常用的并发工具类:CountDownLatch、Semaphore 等等。所以,很多人称 AQS 为 J.U.C 包的核心。那么要想吃透 J.U.C 的源码思想,第一步要做的当然就是彻底弄懂 AQS 啊!
AQS 整体上来看有两个核心组成部分:
我们接下来具体的来看一下这两个重要的组成部分。
一、同步状态
private volatile int state;
我们可以看到,成员变量 state 是被关键字 volatile 修饰了的,这里主要是为了保证 state 这个变量在多个线程之间的可见性。
那么所谓的加锁/解锁,其实就是对 state 这个同步状态的获取,类似于操作系统中实现进程同步的信号量,多个进程通过检查信号量的值来判断是否可以进入临界区。
同步器的主要使用方式是继承,子类通过继承 AQS 并实现它的抽象方法来管理同步状态,在抽象方法的实现过程中免不了要对同步状态进行更改,这时就需要使用同步器提供的三个方法来操作同步状态:
源码如下:
protected final int getState() {
return state;
}
protected final void setState(int newState) {
state = newState;
}
protected final boolean compareAndSetState(int expect, int update) {
// See below for intrinsics setup to support this
return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}
我们主要来看一下 compareAndSetState(int expect, int update) 这个方法,看名字你就能知道,只要以 ”CompareAndxxxxx “ 开头的,基本上就是使用 unsafe 类的 CAS 操作没跑了,主要是为了保证设置同步状态的操作原子性。
二、同步队列
AQS 通过内置的 FIFO 同步队列来完成资源获取线程的排队工作,如果当前线程获取同步状态失败(锁)时,AQS 则会将当前线程以及等待状态等信息构造成一个节点(Node)并将其加入同步队列,同时会阻塞当前线程,当同步状态释放时,则会把节点中的线程唤醒,使其再次尝试获取同步状态。
本文旨在先对 AQS 做一个简单的介绍,同步队列的具体内容在该系列的下一篇文章中。
同步器的设计是基于模板方法模式的,也就是说,使用者需要继承同步器并重写指定的方法,随后将同步器组合在自定义同步组件的实现中,并调用同步器提供的模板方法,而这些模板方法将会调用使用者重写的方法。
那么,接下来我们就来看看,哪些方法是可以重写的,哪些方法是模板方法。
一、可重写方法:
在介绍可重写方法之前,要先让大家明确一点,那就是以下这些方法,不是需要你全部重写(所以这里说的是可重写),这需要根据你想要实现锁的类型(独占锁还是共享锁)来决定你需要重写的方法。
我们可以看出来,这些开发人员可以重写的方法,几乎都是围绕着同步状态的获取与释放来提供的,我们来看一下 AQS 中的源码。
protected boolean tryAcquire(int arg) {
throw new UnsupportedOperationException();
}
protected boolean tryRelease(int arg) {
throw new UnsupportedOperationException();
}
protected int tryAcquireShared(int arg) {
throw new UnsupportedOperationException();
}
protected boolean tryReleaseShared(int arg) {
throw new UnsupportedOperationException();
}
protected boolean isHeldExclusively() {
throw new UnsupportedOperationException();
}
这些方法均未被 abstract 关键字修饰,而笔者在网上看到的好多文章在介绍这部分内容时说的都非常不严谨,直接就说这些方法是待重写的抽象方法,可我们打开 JDK 看 AQS 源码,这里面除了用过 abstract 修饰类,可曾看到任意一个方法是抽象方法了?
这么做是为了保证锁的实现更加方便,在实现某种单一类型的锁的时候不需要重写不相关的方法,比如我现在只想要个互斥锁,而不需要共享锁,那我重写tryAcquireShared(int arg)
与tryReleaseShared(int arg)
有意义吗?所以这些方法都不是被 abstract 修饰过的抽象方法。
但是这些方法的权限修饰符都是 protected 的,这很明显是建议开发人员去 Override 的,而且方法体里面,都仅仅抛出一个 UnsupportedOperationException 异常。
二、模板方法:
同步器提供的模板方法基本上分为以下三类:
这些模板方法,可以称之为顶层方法,什么意思呢,举个例子,比如一个独占锁,实际上进行加锁时,会先调用 acquire 方法,然后 acquire 会调用该独占锁的 tryAcquire 方法。具体点,我们拿 ReentrantLock 来看这个加锁的方法调用过程。
Lock lock = new ReentrantLock();
lock.lock();
上述代码是一个非常常见的使用 ReentrantLock 来获取同步锁的过程,我们来看看 lock()
方法的源码。
public void lock() {
sync.lock();
}
调用的是 sync 的 lock()
方法,我们先来看一看 sync 是啥。
private final Sync sync;
abstract static class Sync extends AbstractQueuedSynchronizer {...}
static final class NonfairSync extends Sync {...}
static final class FairSync extends Sync {...}
静态内部类 Sync 就是 ReentrantLock 的自定义同步器,但是我们发现 Sync 还是一个抽象类,这是因为 ReentrantLock 支持公平锁与非公平锁两种工作模式,具体的同步器由上面的NonfairSync
与FairSync
来实现,通过构造函数来指定,这里不再过多的展开介绍了。我们直接来看源码。
public ReentrantLock() {
sync = new NonfairSync();
}
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
我们发现,ReentrantLock 默认就是使用非公平锁,我们就拿非公平锁来说吧,刚才说到 ReentrantLock 的 lock() 方法实际上是调用的 sync 的 lock() 方法,那么我们就去看看 NonfairSync 中的 lock() 方法具体实现细节。
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
这里解释一下,if 条件内主要是快速的获取锁,如果 CAS 操作成功,那么直接获取到锁。否则就会执行了 AQS 的模板方法啦!那就是 acquire(int arg)
方法。我们接着往下看。
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
看到了吧,执行acquire(int arg)
方法后,会调用tryAcquire(int arg)
方法,而这个方法就是 ReentrantLock 重写的方法!到这儿相信你也对模板方法这个设计模式有了更好的了解。
最后我们来了解一下 AQS 的使用示例吧,其实 Doug lea 老爷子在 AQS 的源码的注释中给我们留了一个简单的 demo,大概在第 200 行左右,我们就拿这个来举例吧。
class Mutex implements Lock, java.io.Serializable {
*
* // 静态内部类,自定义同步器
* private static class Sync extends AbstractQueuedSynchronizer {
* // 是否处于占用状态
* protected boolean isHeldExclusively() {
* return getState() == 1;
* }
*
* // 当状态为0的时候获取锁
* public boolean tryAcquire(int acquires) {
* assert acquires == 1; // Otherwise unused
* if (compareAndSetState(0, 1)) {
* setExclusiveOwnerThread(Thread.currentThread());
* return true;
* }
* return false;
* }
*
* // 释放锁,将状态设置为0
* protected boolean tryRelease(int releases) {
* assert releases == 1; // Otherwise unused
* if (getState() == 0) throw new IllegalMonitorStateException();
* setExclusiveOwnerThread(null);
* setState(0);
* return true;
* }
*
* // 返回一个Condition,每个condition都包含了一个condition队列
* Condition newCondition() { return new ConditionObject(); }
*
* // 反序列化
* private void readObject(ObjectInputStream s)
* throws IOException, ClassNotFoundException {
* s.defaultReadObject();
* setState(0); // reset to unlocked state
* }
* }
*
* // sync把所有脏活累活都干了,我们仅需要将操作代理到Sync上即可.
* private final Sync sync = new Sync();
*
* public void lock() { sync.acquire(1); }
* public boolean tryLock() { return sync.tryAcquire(1); }
* public void unlock() { sync.release(1); }
* public Condition newCondition() { return sync.newCondition(); }
* public boolean isLocked() { return sync.isHeldExclusively(); }
* public boolean hasQueuedThreads() { return sync.hasQueuedThreads(); }
* public void lockInterruptibly() throws InterruptedException {
* sync.acquireInterruptibly(1);
* }
* public boolean tryLock(long timeout, TimeUnit unit)
* throws InterruptedException {
* return sync.tryAcquireNanos(1, unit.toNanos(timeout));
* }
* }}
上述示例中,独占锁 Mutex 是一个自定义同步组件,它在同一时刻只允许一个线程占有 锁。Mutex 中定义了一个静态内部类,该内部类继承了同步器并实现了独占式获取和释放同步状态。在 tryAcquire(int acquires) 方法中,如果经过 CAS 设置成功(同步状态设置为1),则代表获取了同步状态,而在 tryRelease(int releases) 方法中只是将同步状态重置为 0。
用户使用 Mutex 时并不会直接和内部同步器的实现打交道,而是调用 Mutex 提供的方法,在 Mutex 的实现中,以获取锁的 lock() 方法为例,只需要在方法实现中调用同步器的模板方法 acquire(int args) 即可,当前线程调用该方法获取同步状态失败后会被加入到同步队列中等待,这样就大大降低了实现一个可靠自定义同步组件的门槛。