AQS
以下内容可参考美团技术团队:从ReentrantLock的实现看AQS的原理及应用
讲到ReentrantLock就不得不讲AQS,因为Lock的底层就是基于AQS来实现的。那么。什么时AQS呢?
AQS全称AbstractQueuedSynchronizer,是JUC中的一个类。它提供了原子式管理同步状态、阻塞和唤醒线程功能以及队列模型的简单框架。常见的ReentranrLock、Semaphore、CountDownLatch、ThreadPoolExecutor中都是用到了这个类。
AQS类源码简单解释
首先要看一下AQS类中的一些属性和方法及其作用。
属性:
-
state:同步状态,该属性用于表示当前锁是独占锁还是共享锁,也表示当前临界资源由多少个线程获得了锁。初始状态state=0;如果state=1表示为独占锁;如果state>1表示是共享锁。可以看一下
ReentranrLock
和Semaphore
在创建时时如何设置该属性的(以非公平锁为例)。ReentranrLock:该类中NonfairSync内部类最终是继承了AQS,在默认创建LOCK的时候就会创建这个类,所以只需要看这个类的源码即可。在调用lock()方法的时候就会对sta特进行设置,如下:
final void lock() { // 使用CAS将0修改为1 if (compareAndSetState(0, 1)) setExclusiveOwnerThread(Thread.currentThread()); else acquire(1); }
Semaphore:在初始化的时候就会调用setState()方法进行设置,如下:
// 构造方法 Sync(int permits) { setState(permits); } // 父类(AQS)中的setState方法 protected final void setState(int newState) { state = newState; }
-
head:队列的头节点,在下一部分会讲到。
-
tail:队列的尾节点,在下一部分会讲到。
-
spinForTimeoutThreshold:自旋时间(纳秒)
-
一些Offset:偏移量,不做详解。
方法:
在AQS中由非常多的类,这里只说一些比较常见的方法。
- void acquire(int arg):获取锁资源方法,需要调用tryAcquire()方法,如果成功直接返回,如果失败则将该线程加入等待队列进行排队。
- boolean tryAcquire(int arg):未实现方法需要实现类重写;尝试一次获取锁资源。
- Node addWaiter(Node mode):将线程节点添加到等待队列中。
- boolean acquireQueued(final Node node, int arg):以独占不间断模式获取已在队列中的线程。
- cancelAcquire(Node node):取消正在获取锁资源的请求。
- Node enq(final Node node):将就点加入到队列中,必要时初始化队列。
- boolean release(int arg):释放锁资源,会调用tryRelease()方法
- boolean tryRelease(int arg),为实现方法,需要实现类进行重写。
- ......
内部类Node简单解释
在AQS中有一个非常重要的内部类Node类。顾名思义,表示节点。那么什么东西需要节点呢?这里就需要说到AQS中的一个CLH(Craig,Landin,and Hagersten)队列,他是一个虚拟的双端队列,使用Node进行连接。AQS中就有同步队列(Sync Queue)、和条件队列(Condition Queue)。条件队列在用到Condition时才使用,否则可不需要,这里不做介绍。
上面AQS的属性中讲到head、tail属性,head就是同步队列的头节点,tail就是同步队列的尾节点。注意:同步队列的头节点不存放任何有效信息,只是作为队列的初始化节点。
Node中的属性主要有如下:
// 共享模式
static final Node SHARED = new Node();
// 独占模式
static final Node EXCLUSIVE = null;
// 等待状态
// 线程已取消,唯一大于0的状态值
static final int CANCELLED = 1;
// 表示后继线程需要unparking
static final int SIGNAL = -1;
// 线程正在等待条件
static final int CONDITION = -2;
// 下一个 acquireShared 应该无条件传播
static final int PROPAGATE = -3;
// 当代状态,主要由上面四种
volatile int waitStatus;
// 前驱节点
volatile Node prev;
// 后继节点
volatile Node next;
// 节点包含的的线程
volatile Thread thread;
// 等待条件的下一个节点
Node nextWaiter;
主要需要了解waitStatus
属性的取值类型,且0为节点初始化默认值。
以实例的方式剖析原理——获取锁的过程
以下步骤可能比较繁琐,但是动手在IDE中一步一步走下来还是非常好理解的。
第一层:ReentrantLock获取锁
ReentrantLock lock = new ReentrantLock(); // 创建ReentrantLock实例对象
// 获取锁
lock.lock();
try{
// TODO
}
第二层:ReentrantLock类中的操作
通过源码可以知道,默认情况下创建的lock对象是一个非公平锁,所以这里就以非公平锁为例。至于ReentrantLock类的其他具体可参考源码。初始化如下:
// ReentrantLock构造方法
public ReentrantLock() {
sync = new NonfairSync(); // 否公平锁
}
所以后续的lock操作可以说都是依赖于NonfairSync这个内部类进行操作,如下:
// 将会调用上面NonfairSync中的lock方法
public void lock() {
sync.lock();
}
第三层:NonfairSync内部类的操作
// NonfairSync内部类中的方法
final void lock() {
if (compareAndSetState(0, 1))
// 获取锁成功,将当前线程设置为该锁的独占线程
setExclusiveOwnerThread(Thread.currentThread());
else
// 否则,执行acquire逻辑
acquire(1);
}
这里可以看到if中会使用CAS对state的状态进行设置(注意:NonfairSync类最终继承自AQS,所以继承有states属性)。state属性前面已经讲过——表示锁的状态,所以这里就是将0设置为1(1表示独占),如果成功表示获取锁成功并直接返回;否则将执行acquire()方法逻辑。
第四层:AQS的acquire()方法。
acquire()方法时AQS的核心方法之一,用于无视中断尝试获取锁资源。源码如下:
// AQS类中的qcquire方法
public final void acquire(int arg) {
if (!tryAcquire(arg) && // 尝试获取锁资源
acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) // 添加节点到同步队列中
selfInterrupt();
}
这里有两个逻辑,分别是tryAcquire()尝试获取锁资源,acquireQueued()让同步队列中的节点不断尝试获取锁资源。
如果tryAcquire()返回true表示获取锁资源成功,也就不需要执行后续操作。
第五层:tryAcquie()方法的实现
其中tryAcquire()是一个为实现方法,需要子类具体实现,可以看到NonfairSync的父类Sync是如何实现的。
// AQS中的tryAcquire未实现
protected boolean tryAcquire(int arg) {
throw new UnsupportedOperationException();
}
// ReentrantLock类的内部类Sync中的方法对tryAcquire进行了具体实现
// 参数为1,依然表示独占锁
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState(); // 获取同步状态
if (c == 0) {
if (compareAndSetState(0, acquires)) {
// 如果状态为0且CAS设置状态成功,则表示获取锁成功,返回true即可
setExclusiveOwnerThread(current);
return true;
}
}
// 如果状态不为0表示已有线程占有了锁,所以判断这个线程是不是当前线程,是则表示当前线程重入了,所以在原基础上+1即可,这也是lock可以如所实现的原理。
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
// 如果获取失败,则返回false,执行后续操作
return false;
}
通过上面需要直到两点:
- 如果锁资源没有被占有,则tryAcquire会尝试CAS设置同步状态以达到获取锁的目的。
- 如果当前线程再次获取该锁资源,则将同步状态在原基础上+1即可,也就是lock可重入锁的实现原理。
如果返回true则第四层中的&&后面操作将不再继续,否则执行后面的操作。
第六层:同步队列中的节点获取锁资源
如果第五步中的tryAcqiure()获取失败,则会执行到这一步。这里需要关注两个方法:acquireQueued()和addWaiter()方法。
先来看addWaiter()
方法:
// AQS类中的addWaiter方法
// 参数为Node.EXCLUSIVE,即null,独占锁
private Node addWaiter(Node mode) {
// 将当前线程封装为一个同步队列的节点并设置模式为独占式
Node node = new Node(Thread.currentThread(), mode);
// 获取并临时保存尾节点
Node pred = tail;
// 如果尾节点不为空,表示同步队列已存在
if (pred != null) {
// 将当前线程节点的前驱设置为尾节点
node.prev = pred;
// 使用CAS将当前线程的节点设置为尾节点
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
// 如果同步队列未初始化
enq(node);
return node;
}
/////////////////////// enq方法 ///////////////////////////
// 参数为当前线程所封装的节点
private Node enq(final Node node) {
// 无限循环直到添加节点到同步队列成功
for (;;) {
Node t = tail;
// 初始化同步队列
if (t == null) { // Must initialize
// 如果尾节点为空,则初始化一个空节点,并将其这是为头节点和尾节点
if (compareAndSetHead(new Node()))
tail = head;
} else { // 添加当前线程的节点到同步队列的最后
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
通过上面的两个方法我们可以清楚的看到,这里的主要操作就是:将当前线程封装为Node节点并将其添加到同步队列的尾部,而enq()方法主要是应对同步队列不存的情况。
再来看看acquireQueued()
方法:
// AQS中的acquireQueued方法
// 参数:当前线程节点,1
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
// 中断标记
boolean interrupted = false;
for (;;) {
// 获取当前线程节点的前驱节点
final Node p = node.predecessor();
// 如果前驱节点是头节点,则使当前线程尝试获取锁资源(tryAcquire方法忘了回头看第五步)
if (p == head && tryAcquire(arg)) {
// 如果当前程线程获取锁资源成功,则将当前线程节点设置为头节点
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
// 根据前驱节点p的等待状态判断是否要将当前线程阻塞
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
// 生成CANCELLED状态节点,并唤醒节点
if (failed)
cancelAcquire(node);
}
}
该方法也是比较好理解:在将当前线程的节点添加到同步队列之后,判断当前线程是不是队列第二个节点(第一个节点不存放有效信息),则让当前线程尝试获取锁资源,成功则完事大吉,否则执行最后的尝试操作。
需要执行shouldParkAfterFailedAcquire方法和parkAndCheckInterrupt方法。
所以acquireQueued()方法的核心就是让同步队列中节点不断尝试获取锁资源
第七层:获取锁资源的最后底线
这里主要关注shouldParkAfterFailedAcquire方法和parkAndCheckInterrupt两个方法。
先看shouldParkAfterFailedAcquire()
方法:
// AQS中的shouldParkAfterFailedAcquire方法
// 参数:当前线程节点的前驱节点,当前线程节点
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
// 获取前驱节点的等待状态 (忘了可见Node内部类的属性定义)
int ws = pred.waitStatus;
// 如果使SIGNAL状态,则进行park操作
if (ws == Node.SIGNAL)
/*
* This node has already set status asking a release
* to signal it, so it can safely park.
*/
return true;
// 如果线程为CANCELLED,则将其从同步队列中剔除
if (ws > 0) {
/*
* Predecessor was cancelled. Skip over predecessors and
* indicate retry.
*/
do {
// 该行逻辑为:将当前线程的前一个节点设置为上上个节点,即跳过上一个节点
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0); // 循环来看,就是跳过当前线程节点之前所有的CANCELLED节点
pred.next = node;
} else {
/*
* waitStatus must be 0 or PROPAGATE. Indicate that we
* need a signal, but don't park yet. Caller will need to
* retry to make sure it cannot acquire before parking.
*/
// 否则将前驱节点的等待状态设置为SIGAL,也就是能够park
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
// 返回false
return false;
}
该方法主要操作是判断节点的等待状态,如下:
- 判断当前线程的前驱节点的等待状态是否为SIGNAL,如果是则返回true并执行后面的parkAndCheckInterrupt()方法。
- 判断当前线程的前驱节点的等待状态是否为CANCELLED,是则剔除当前线程节点之前所有的连续的该状态节点。
- 以上都不是,则将前驱节点的等待状态设置为SIGNAL,最后返回false;
流程图如下(来自美团技术团队):
再来看parkAndCheckInterrupt()
方法:
// AQS的parkAndCheckInterrupt方法
private final boolean parkAndCheckInterrupt() {
// park当前线程
LockSupport.park(this);
return Thread.interrupted();
}
该方法很简单,就是park当前线程,即阻塞当前线程,并返回线程的中断状态。这一步的目的是:根据前驱节点的等待状态判断是否需要阻塞当前线程,避免无限循环浪费CPU资源。
到此基本的上锁操作完成,回到acquireQueued方法,逻辑如下图(来自美团技术团队):
不过还有一处地方需要思考:acquireQueued中的finally代码块,通过源码查看我们可以看到正常情况下finally中的cancelAcquire()一辈子不会执行,所以只有在异常情况下会执行,在异常情况下就会将当前线程的所资源请求操作取消并设置为CANCELLED状态。源码略。归结起来有三种情况:
- 当前线程节点是尾节点:直接剔除当前节点;
- 当前线程节点是既不是尾节点,也不是头节点的后继节点:略过当前节点,当前节点的前驱指向当前节点后继节点;
- 当前节点是头节点的后继:唤醒当前节点之后最近的一个非CANCELLED状态的节点,实现方式是从尾节点开始向前遍历,找到最靠近的节点。
获取锁过程大总结:
- 首先使用lock.lock()的时候会调用NonfairSync中的lock方法,并尝试CAS这是同步状态获取锁资源;如果成功则获取锁成功,否则下一步。
- 调用AQS中的acquire()方法,该方法会先调用tryAcquire()方法尝试回去锁资源(该方法由AQS子类具体实现),这里会判断是否是当前线程的重入锁,成功直接返回,失败进入下一步。
- 获取失败之后会调用addWaiter()方法将当前线程添加到同步队列的尾部,然后调用acquireQueued()方法使同步队列中的节点获取锁资源:如果当前线程是head节点的后继节点(第二个节点)则尝试获取锁资源,成功则将当前节点设置为头节点并返回,否则下一步。
- 调用shouldParkAfterFailedAcquire()方法,如果当前节点的前驱是SIGNAL状态则调用parkAndCheckInterrupt()方法阻塞当前线程,如果是CANCELLED状态则去除当前节点之前的CANCELLED状态的节点,否则将前驱节点设置为SIGNAL,然后回到上一步再次尝试获取锁资源。
场景模拟:线程A先获取锁资源,线程B再获取锁资源,线程C最后获取锁资源。
- 线程A先通过lock设置了state同步状态并获取到锁;
- 线程B来获取锁,lock设置失败,调用acquire(),tryAcquire()失败,调用addWaiter()添加到同步队列,调用acquireQueued()方法,由于是head的后继节点则一尝试获取锁资源成功返回,否则调用shouldParkAfterFailedAcquire()方法将头节点这是为SIGNAL,下一次再执行这个方法的时候线程B就会被阻塞;
- 线程C来获取锁,也进入到acquired()方法,但是它不是head的后继节点,所以直接调用shouldParkAfterFailedAcquire()方法将线程B设置为SIGNAL状态,下一次循环中调用parkAndCheckInterrupt()方法将线程C阻塞。
以实例的方式剖析原理——释放锁的过程
第一步:ReentrantLock释放锁
finally{
lock.unlock();
}
接着会调用Sync类中的release()
public void unlock() {
sync.release(1); // 其实是父类AQS的方法
}
第二步:AQS中的release
// AQS中的release
// 参数:1
public final boolean release(int arg) {
// 调用tryRelease尝试释放锁
if (tryRelease(arg)) {
Node h = head;
// 如果头节点不为null且等待状态不为0
if (h != null && h.waitStatus != 0)
// 唤醒头结点的后继节点线程,将会调用LockSupport.unpark()
unparkSuccessor(h);
return true;
}
return false;
}
tryRelease释放锁:
与acquire()类似,AQS中的tryRelease()也没有实现,需要子类实现,看到Sync中的tryRelease重写方法:
// Sync重写方法
// 参数:1
protected final boolean tryRelease(int releases) {
// 释放锁,所以要将同步状态-1
int c = getState() - releases;
// 如果不是当前线程则抛出异常,监视器状态异常
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
// 如果为0,没有线程占有锁
if (c == 0) {
free = true;
// 设置独占线程为null
setExclusiveOwnerThread(null);
}
// 设置新的同步状态
setState(c);
return free;
}
综上,可以看到释放锁的过程还是比较简单的:当修改完同步状态成功之后,只需要判断头节点的状态即可唤醒线程。
问题1:为什么要是(h != null && h.waitStatus != 0)?
- h != null 好理解,防止空指针;
- h.waitStatus != 0 呢,其实当一个线程被new为一个Node的时候waitStatus默认就是0,只有在获取锁的时候调用了shouldParkAfterFailedAcquire()方法之后会将其设置为SIGNAL。所以可以理解,当waitStatus==0的时候其后继节点一定没有被park,所以也就不需要唤醒了。否则就需要执行唤醒操作。
问题2:tryRelease()中的c会大于1吗?
- 会,这就是可重入锁的概念,一个线程重入了n次,c就会是n。因此只有c==0的时候才会返回true表示释放锁资源完成。
以上就是ReentrantLock中使用AQS实现获取锁释放锁的大致过程,更多内容好需要进一步仔细研读源码。