在技术面试的时候,“说下你对 AQS 的理解”,这个问题出现的概率属实不低,而一些技术底子一般的同学,又非常容易被它复杂的底层源码弄得晕头转向。
今天这篇文章,我们就以做减法的方式,将这个知识点彻底地大家讲清楚。
AQS,是 AbstractQueuedSynchronizer(抽象队列同步器)这个类的简称,也是 Java JUC 包中的灵魂,ReentrantLock、ReentrantReadWriteLock、Semaphore、CountDownLatch、CyclicBarrier 都是通过其实现锁或同步器的。
其核心思想为,在多线程并发访问共享资源时,通过双向链表实现的先进先出 CLH 队列进行线程排队,并通过由 volatile 修饰的 state 变量来标识资源的锁占用状态。
如下图所示:
图片
在 AQS 中提供了两种资源获取方式:
独占模式(Exclusive),在同一时刻只能有一个线程获取竞态资源,比如:ReentrantLock。
共享模式(Share),在同一时刻可以有多个(参数设定)线程获取竞态资源,比如:CountDownLatch、Semaphore。
AQS 方法详述
AQS 的方法大致分为三类,分别是独占模式下的方法、共享模式下的方法、通过模板方法模式留给子类实现的方法。
独占模式:
// 获取锁
public final void acquire(int arg)
// 以可中断的方式获取锁
public final void acquireInterruptibly(int arg)
// 以带超时时间的方式,尝试获取锁
public final boolean tryAcquireNanos(int arg, long nanosTimeout)
// 释放锁
public final boolean release(int arg)
共享模式:
// 获取锁
public final void acquireShared(int arg)
// 以可中断的方式获取锁
public final void acquireSharedInterruptibly(int arg)
// 以带超时时间的方式,尝试获取锁
public final boolean tryAcquireSharedNanos(int arg, long nanosTimeout)
// 释放锁
public final boolean releaseShared(int arg)
需要子类实现的方法:
// 尝试获取独占锁
protected boolean tryAcquire(int arg);
// 尝试释放独占锁
protected boolean tryRelease(int arg);
// 尝试获取共享锁
protected int tryAcquireShared(int arg);
// 尝试释放共享锁
protected boolean tryReleaseShared(int arg);
// 判断当前线程是否正在持有锁
protected boolean isHeldExclusively();
看到 AQS 父类实现了一部分方法,也预留了一些方法让 ReentrantLock、CountDownLatch、Semaphore、CyclicBarrier 等子类实现,我们想到了哪种设计模式?
是的,模板方法模式。
模板方法模式:定义一个操作中算法的框架,而将一些步骤延迟到子类中,模板方法使得子类可以不改变一个算法的结构,即可重定义该算法的某些步骤。
使用模板方法模式,可以将一个操作的复杂流程的实现步骤进行拆分,封装在一系列基本方法中,在抽象父类提供一个模板方法来定义整体执行步骤,而通过其子类来覆盖某个步骤,从而使得相同的执行步骤可以有不同的执行结果。
类结构如下:
图片
模板方法模式的优点在于:
- 代码复用性高,父类的模板方法和具体方法都可以供多个子类复用。
- 代码灵活性高,可根据业务迭代情况,灵活选择哪部分复用父类具体方法,哪部分进行子类覆盖实现。
嗯,这些底层源码的设计还是非常巧妙的,而设计模式本身也并不是有些人口中的过度设计的“花架子”。
ReentrantLock 与 AQS
接下来我们看下,ReentrantLock 是如何通过 AQS 来实现锁机制的。
两者间的 UML 图如下所示:
图片
从图中可以看到,ReentrantLock 中有一个 Sync 内部类,Sync 继承自 AQS,且 Sync 有两个子类 FairSync 和 NonfairSync,分别用于实现公平锁和非公平锁。
我们梳理一下源码,看看 ReentrantLock 如何实现非公平锁的。
代码入口如下,我们看只有两个方法加上一个判断。
public class ReentrantLock implements Lock, java.io.Serializable{
abstract static class Sync extends AbstractQueuedSynchronizer {
final void lock() {
if (!initialTryLock())
acquire(1);
}
}
}
}
来看下该方法的具体实现,简而言之,该方法尝试以独占的方式获取锁。
static final class NonfairSync extends Sync {
final boolean initialTryLock() {
Thread current = Thread.currentThread();
if (compareAndSetState(0, 1)) {
// first attempt is unguarded
setExclusiveOwnerThread(current);
return true;
} else if (getExclusiveOwnerThread() == current) {
int c = getState() + 1;
if (c < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(c);
return true;
} else
return false;
}
}
先是通过 compareAndSetState(0, 1) 方法,以原子操作的方式将 AQS 类中的 state 变量值从 0 修改到 1。
我们在上文中提到过,state 变量来标识资源的锁占用状态,0 代表未占用,1 代表已占用,大于 1 则代表锁被重入,那么该操作就是尝试获取锁。
若该操作执行成功,则通过 setExclusiveOwnerThread(current) 作用是将当前线程设置为持有独占锁的线程,并返回 true,代表获取锁成功了。
再往下分析 getExclusiveOwnerThread() == current,这是判断当前线程是否已获取该锁且处于未释放的状态,若判断成立则将 state 变量+1代表重入,并返回 true 表示获取锁成功。
btw:从这段代码逻辑上看,知道为什么叫非公平锁了吧,一上来并没有通过 AQS 排队,而且先去争抢锁。
接下来我们继续来看acquire(1)方法,代码如下:
public abstract class AbstractQueuedSynchronizer extends AbstractOwnableSynchronizer implements java.io.Serializable {
public final void acquire(int arg) {
if (!tryAcquire(arg))
acquire(null, arg, false, false, false, 0L);
}
}
方法体重有两个方法加上一个判断,先来看 tryAcquire(arg) 方法的执行逻辑。
static final class NonfairSync extends Sync {
protected final boolean tryAcquire(int acquires) {
if (getState() == 0 && compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(Thread.currentThread());
return true;
}
return false;
}
}
这块代码的逻辑竟然跟上面 initialTryLock() 方法的前半段几乎一样,先是通过 compareAndSetState(0, 1) 方法将 AQS 类中的 state 变量值从 0 修改到 1。
若该操作执行成功,则通过 setExclusiveOwnerThread(current) 作用是将当前线程设置为持有独占锁的线程,并返回 true,代表获取锁成功了。
btw:果然是非公平锁啊,这是誓要将插队争抢锁进行到底了。
下面就是 AQS 中的重头戏了,acquire(null, arg, false, false, false, 0L)方法,实现排队获取锁。
代码实现如下:
图片
这块代码并非主业务链路,先是进行了三个判断,当前节点不是 first 节点和 head 节点,且前驱结点不为null。
btw:head 节点可以理解为“当前持有独占锁的线程”,而在 head 节点之后的线程都处于阻塞、等待被唤醒的状态,而 first 节点则是同步队列中第一个等待获取锁的线程。
接下来 pred.status < 0 代表前驱节点已经被取消,结果为 true 则做一次等待队列清理。
而 pred.prev == null 则是判断前驱节点是否为 null,结果为 true 则跳到下一次循环中。
图片
这段代码的意思是,在当前节点为 first 节点或前驱节点为 null (未入队)的情况下,继续通过 tryAcquire(arg) 方法尝试获取锁。
图片
这段代码看起来比较复杂,其实也是有逻辑性的。
1、前两个大的逻辑分支判断的意思是,先创建一个独占节点,并将该节点加入到 CLH 队列的尾部。
2、如果当前节点为 first 节点,且自旋数大于 0,则继续尝试自旋获取锁。
3、将当前节点的状态值设置为“等待中”。
4、当前节点自旋失败,使用 LockSupport.pack() 方法挂起线程。
5、当独占锁被释放,队列中的 first 节点的线程将被唤醒,清除当前节点的等待状态。