AQS是类AbstractQueuedSynchronizer的缩写,是一个队列式的同步器,功能是用于限制多线程对资源的访问,在java中,很多与锁或者同步相关的类都依赖AQS,比如ReentrantLock、Semaphore等。对于如何访问共享资源和独占资源,它提供了一个基本的框架。
文章目录
一、基本原理
AQS内部有一个类似于锁或者通行证的属性字段,线程每次访问资源前,都需要调用AQS中的方法获得这个锁,如果没有拿到锁,则将该线程放入一个等待队列的尾部,并置线程状态为waiting
,线程进入休眠状态。
资源访问结束后,线程需要调用相应的方法释放锁,同时从等待队列中找到第一个进入队列的线程将其唤醒,该被唤起的线程就开始尝试获得锁,如果能拿到锁,就开始运行,如果拿不到就继续等待。
下面重点介绍一下等待队列。
AQS将没有拿到锁的线程都放入一个等待队列中,这个等待队列是按照申请锁的先后顺序排列的,遵循先进先出。AQS的等待队列是CLH锁队列(CLH lock queue)的一个变种,CLH是三个人的名字缩写:Craig, Landin和Hagersten。CLH锁队列使用CAS和自旋锁,既兼顾了性能,又可以不让线程长期保持“饥饿”状态。CLH锁队列如下图:
该队列是一个双向队列,除了头结点之外的其他节点都代表了一个等待锁的线程,头结点在大部分情况下都代表一个持有锁且正在运行的线程。
二、源码详解
1、准备知识
在AbstractQueuedSynchronizer中,使用内部类Node表示AQS队列中一个节点:
//代码有删减
static final class Node {
//节点状态
volatile int waitStatus;
//指向队列的前一个节点
volatile Node prev;
//指向队列的后一个节点
volatile Node next;
//记录当前线程
volatile Thread thread;
Node nextWaiter;
//获得队列的前一个节点
final Node predecessor() throws NullPointerException {
Node p = prev;
if (p == null)
throw new NullPointerException();
else
return p;
}
Node(Thread thread, Node mode) { // Used by addWaiter
this.nextWaiter = mode;
this.thread = thread;
}
}
每个Node对象都一个Thread属性,因此每个Node节点都代表了一个等待锁的线程。
属性waitStatus表示节点状态,它有如下几个取值:
- CANCELLED(1):表示当前结点已取消申请锁。当timeout或被中断(响应中断的情况下),会触发变更为此状态,进入该状态后的结点将不会再变化。
- SIGNAL(-1):表示后继结点在等待当前结点唤醒。后继结点入队时,会将前继结点的状态更新为SIGNAL。
- CONDITION(-2):表示结点等待在Condition上,当其他线程调用了Condition的signal()方法后,CONDITION状态的结点将从等待队列转移到同步队列中,等待获取同步锁。
- PROPAGATE(-3):共享模式下,当前驱节点申请到锁或者释放锁时都可以唤醒后继节点,后继节点还可以继续唤醒它的后继节点,这样依次往后传播,该值只在共享模式下doReleaseShared()方法中使用。
- 0:新结点入队时的默认状态。
从取值上可以看到,当waitStatus>0时,只有一个CANCELLED状态,在代码里面有很多地方使用waitStatus>0
判断节点状态是否正常。
在AbstractQueuedSynchronizer中,锁使用属性state表示:
private volatile int state;
严格来说,state不是锁,在不同是子类里面,它表示不同的含义,比如子类ReentrantLock,state初始值为0,线程如果加锁成功,那么state加1,线程解锁的话,state减1,当state=0时,表示线程不在持有锁;在子类Semaphore,state初始为信号量的个数,线程每获得一次信号量,state就减去相应的值,当state=0时,便阻塞线程。
AbstractQueuedSynchronizer名字中带有Abstract表示该类是一个抽象类,该类中并没有抽象方法,但是根据子类的应用场景,有几个方法需要重写:
//独占模式下,尝试加锁,如果成功,返回true,失败返回false
protected boolean tryAcquire(int arg) {
throw new UnsupportedOperationException();
}
//独占模式下,尝试释放锁,如果全部释放,返回true,否则返回false
protected boolean tryRelease(int arg) {
throw new UnsupportedOperationException();
}
//共享模式下,尝试加锁,返回负数表示加锁失败;
//0代表获取成功,但没有剩余锁;正数表示获取成功,还有剩余锁,其他线程还可以去获取。
protected int tryAcquireShared(int arg) {
throw new UnsupportedOperationException();
}
//共享模式下,尝试释放锁,如果全部释放,返回true,否则返回false
protected boolean tryReleaseShared(int arg) {
throw new UnsupportedOperationException();
}
//检查当前线程是否处于独占模式,如果是,返回true。只有用到Condition才需要去实现它。
protected boolean isHeldExclusively() {
throw new UnsupportedOperationException();
}
AQS中将需要覆盖的方法定义为protected,且默认实现都是抛异常的。这些方法分为了两种模式:共享模式和独占模式。
- 共享模式指的是多个线程共享资源,比如类Semaphore,允许多个线程同时访问资源,可以认为有多把锁,每个线程可以申请一个或多个,凡是申请到的都可以访问资源;
- 独占模式指的是每个资源同时只能一个线程访问,比如ReentrantLock,如果一个线程加锁成功,其他的线程禁止访问。
如果应用场景是线程独占资源,那么子类只实现tryAcquire()/tryRelease()两个方法即可,如果是共享资源,那么子类实现tryAcquireShared()/tryReleaseShared()。
通过这些方法的含义,可以知道子类里面只需要考虑如何加锁和释放锁,至于AQS队列的维护和选择哪个线程尝试获取锁,都是AQS帮我们处理了,我们不需要关心。
下面分独占模式和共享模式,分别介绍一下AQS中的方法,从中可以知道AQS如何维护队列,以及各个线程是如何争抢资源的。
2、独占模式申请锁:acquire()
acquire()方法是在独占模式下使用的,如果线程想要访问独占资源,需要调用该方法,如果成功,则返回,如果资源正在被其他线程访问,那么该方法会阻塞线程。ReentrantLock.lock()方法便是调用该方法完成的加锁。该方法会忽略线程中断。
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
acquire()方法里面首先调用tryAcquire()方法,尝试加锁,如果加锁失败,则调用addWaiter():
private Node addWaiter(Node mode) {
//使用当前线程和独占模式做为参数创建Node节点
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;
}
}
//如果上一步CAS操作没有成功,则进入enq方法,使用自旋锁和CAS操作继续执行入队操作
enq(node);
return node;
}
compareAndSetTail()方法里面使用Unsafe类将当前节点设置为队列尾节点,后面凡是涉及到使用Unsafe类的方法不再详细介绍代码:
private final boolean compareAndSetTail(Node expect, Node update) {
return unsafe.compareAndSwapObject(this, tailOffset, expect, update);
}
addWaiter()方法首先尝试将节点放入队列尾,如果不成功,则调用enq()方法:
private Node enq(final Node node) {
//自旋锁,不断循环直到成功
for (;;) {
Node t = tail;
if (t == null) { // Must initialize
//如果当前队列是空的,则创建一个空的Node节点作为头结点
if (compareAndSetHead(new Node()))
tail = head;
} else {
//继续尝试将当前节点设置为队列尾节点
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
调用完addWaiter()方法将节点入队之后,调用acquireQueued()方法:
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
//自旋锁
for (;;) {
//取当前节点的前一个节点
final Node p = node.predecessor();
//如果当前节点是head节点的下一个节点,那么尝试获取锁
//如果p == head为true,说明原来的head节点代表的线程已经释放了锁,
//不过也有可能是当前节点被中断了(Thread..interrupt())
if (p == head && tryAcquire(arg)) {
//将当前节点设置为头结点
setHead(node);
//将前一个节点的next设置为null,是为了JVM能够GC掉原来的头结点
//在setHead()方法里面已经将node节点的prev设置为null了
p.next = null;
failed = false;
return interrupted;
}
//下面介绍shouldParkAfterFailedAcquire()和parkAndCheckInterrupt()
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
//如果线程被中断,则设置返回值为true
interrupted = true;
}
} finally {
if (failed)
//如果上面申请锁的过程中,抛出异常,进入下面的方法
cancelAcquire(node);
}
}
接着看shouldParkAfterFailedAcquire():
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
//创建节点的时候,waitStatus默认是0
int ws = pred.waitStatus;
if (ws == Node.SIGNAL)
//如果前驱节点状态已经设置好了,也就是说已经告知前驱节点我在等着你通知了,
//那么当前线程接下来就可以进入waiting状态了
return true;
if (ws > 0) {
//waitStatus>0表示节点代表的线程放弃了加锁操作,那么当前节点的位置就往前挪
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
//设置前驱节点的状态为SIGNAL,表示后继节点等待前驱节点唤醒
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
parkAndCheckInterrupt():
private final boolean parkAndCheckInterrupt() {
//设置当前线程状态为waiting,当前线程进入休眠状态,等待前驱节点将自己唤醒,或者线程被中断
LockSupport.park(this);
//检查线程是否中断
return Thread.interrupted();
}
下面在回过来看一下acquireQueued()方法里面最后调用的cancelAcquire()方法,如果在尝试加锁的过程中抛出异常,那么会调用该方法:
private void cancelAcquire(Node node) {
//当前节点不存在,忽略
if (node == null)
return;
//设置node节点的线程对象为null,表示线程与node节点解除关联
node.thread = null;
Node pred = node.prev;
//找到前面节点中第一个状态正常的节点
while (pred.waitStatus > 0)
node.prev = pred = pred.prev;
Node predNext = pred.next;
//将当前节点的状态设置为CANCELLED
node.waitStatus = Node.CANCELLED;
//如果当前节点是队列尾,那么调整队列中的节点,将当前节点从队列尾剔除
//如果下面的方法修改不成功,当前节点在后续处理中也不会被唤醒
if (node == tail && compareAndSetTail(node, pred)) {
compareAndSetNext(pred, predNext, null);
} else {
int ws;
if (pred != head &&
((ws = pred.waitStatus) == Node.SIGNAL ||
(ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) &&
pred.thread != null) {
Node next = node.next;
//将当前节点从队列中剔除,并将合法的节点连接起来
//如果当前分支操作失败,//在shouldParkAfterFailedAcquire()方法里面会继续操作
if (next != null && next.waitStatus <= 0)
compareAndSetNext(pred, predNext, next);
} else {
//如果当前节点是队列中第一个等待锁的节点,
//那么需要唤醒下一个节点开始抢锁
unparkSuccessor(node);
}
node.next = node;
}
}
执行完acquireQueued() 方法之后,如果当前线程被中断过,那么调用selfInterrupt()再次中断,相当于之前没有响应中断,这次补上一次:
static void selfInterrupt() {
Thread.currentThread().interrupt();
}
到这里,在独占模式下调用acquire()申请锁访问独占资源的流程介绍完了,这里总结一下:
- 调用tryAcquire方法尝试申请锁,申请成功,则可以直接访问独占资源;
- 如果上一步申请失败,调用addWaiter()方法将当前线程作为等待队列中的节点加入等待队列;
- 检查队列中当前节点的前驱节点是否是头结点,如果是,则再次尝试申请锁,如果成功则将当前节点设置为头结点,接下来可以访问独占资源,如果申请失败,修改前驱节点的状态为SIGNAL,然后线程进入waiting状态,等待被前驱节点唤醒或者线程被中断,前面这一个过程会不断循环,直到锁申请成功或者抛出异常。
- 上一步申请锁的过程中如果抛出异常,会将当前节点从等待队列中删除。
3、独占模式释放锁:release()
上一小节介绍了独占模式下申请锁,这里介绍一下与申请对应的释放锁release():
public final boolean release(int arg) {
if (tryRelease(arg)) {
//head节点代表的是当前线程所在的节点
Node h = head;
//如果waitStatus为0,表示当前节点已经是队列尾,后面没有等待线程了
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
release()首先调用tryRelease()尝试释放锁,如果释放失败,则返回false,释放成功则找到队列的头结点,将头结点后面的等待线程唤醒:
private void unparkSuccessor(Node node) {
int ws = node.waitStatus;
if (ws < 0)
//将当前节点的状态修改为0,我认为这里置不置0,对程序运行没有影响
//如果能将节点状态设置为0,release()方法可以快速返回
compareAndSetWaitStatus(node, ws, 0);
//首先查看当前节点的下一个节点是否是合法状态,如果是,则直接唤醒
//如果不是,则从队列尾向前找,找到一个状态合法的节点,然后将其唤醒
Node s = node.next;
if (s == null || s.waitStatus > 0) {
s = null;
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t;
}
if (s != null)
LockSupport.unpark(s.thread);
}
独占模式下,释放锁的过程比较简单,这里总结一下:
- 调用tryRelease()方法释放锁;
- 如果上一步释放成功,则找到队列的头结点,然后查看下一个节点的状态是否是等待状态,如果是,则唤醒,如果不是,则从队列尾向前找,找到一个等待的线程,然后将其唤醒。
4、共享模式申请锁:acquireShared()
acquireShared()方法是在共享模式下使用的,如果线程想要访问共享资源,需要调用该方法,如果成功,则返回,如果资源正在被其他线程访问,那么该方法会阻塞线程。该方法同样的也会屏蔽线程中断。
public final void acquireShared(int arg) {
if (tryAcquireShared(arg) < 0)
doAcquireShared(arg);
}
从上面方法可以看到,acquireShared()首先调用tryAcquireShared()尝试加锁,如果成功则返回,如果失败则调用doAcquireShared()入等待队列:
private void doAcquireShared(int arg) {
//创建共享模式的节点,并将节点放到等待队列的尾
final Node node = addWaiter(Node.SHARED);
boolean failed = true;
try {
boolean interrupted = false;
//自旋锁
for (;;) {
//找到当前节点的前驱节点
final Node p = node.predecessor();
if (p == head) {
//如果当前节点是head节点的下一个节点,那么尝试获取锁
//如果p == head为true,说明原来的head节点代表的线程已经释放了锁,
//不过也有可能是当前节点被中断了(Thread..interrupt())
int r = tryAcquireShared(arg);
//r大于等于0,说明加锁成功
if (r >= 0) {
//将当前节点设置为头结点,并且如果还有锁,则继续唤醒后面的节点
setHeadAndPropagate(node, r);
p.next = null; // help GC
//检查是否线程中断,如果有,则再调用一次补上中断
if (interrupted)
selfInterrupt();
failed = false;
return;
}
}
//更改前驱节点的状态,如果更改成功,当前线程进入waiting状态,等待被唤醒
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
这个方法和独占模式的acquireQueued()很类似。下面重点看一下setHeadAndPropagate(),与独占模式相比,两者的区别主要在这个方法上:
private void setHeadAndPropagate(Node node, int propagate) {
Node h = head; // Record old head for check below
//将当前节点设置为头结点
setHead(node);
//propagate表示剩余可以使用的锁,因为锁有多个,可以多个线程共同使用
//因此只要下面有一个条件满足,就调用doReleaseShared()尝试唤醒后继节点
if (propagate > 0 || h == null || h.waitStatus < 0 ||
(h = head) == null || h.waitStatus < 0) {
Node s = node.next;
if (s == null || s.isShared())
doReleaseShared();
}
}
private void doReleaseShared() {
for (;;) {
Node h = head;//尝试唤醒头结点的后继节点
if (h != null && h != tail) {
int ws = h.waitStatus;
//如果当前节点的状态为Node.SIGNAL,表示后继节点等待前驱节点唤醒
if (ws == Node.SIGNAL) {
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
continue;
unparkSuccessor(h);//唤醒后继节点
}
//将当前节点的状态设置为Node.PROPAGATE,
//这样可以确保以后申请锁和释放锁时,都可以唤醒节点
else if (ws == 0 &&
!compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
continue; // loop on failed CAS
}
if (h == head) // loop if head changed
break;
}
}
setHeadAndPropagate()主要是将当前唤醒的节点设置为头结点,然后在尝试唤醒后继节点。
理解了独占模式下申请锁的流程,共享模式就会很好理解。都是先申请锁,如果不成功,则加入队列,然后线程进入waiting状态,线程休眠,被其他线程唤醒之后,尝试再尝试申请锁,申请成功了,则将当前节点设置为头结点。区别是,独占模式加锁成功后,不会在唤醒后继节点,而共享模式会继续唤醒后继节点。
5、共享模式释放锁:releaseShared()
共享模式下释放锁需要调用releaseShared()方法:
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}
doReleaseShared()在前面已经介绍过了:
private void doReleaseShared() {
for (;;) {
Node h = head;
//如果队列不为空,则进入下面的if分支
if (h != null && h != tail) {
int ws = h.waitStatus;
//如果后继节点在等待前驱节点唤醒,则进入下面的分支唤醒后继节点
if (ws == Node.SIGNAL) {
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
continue;
unparkSuccessor(h);
}
//将当前节点的状态设置为Node.PROPAGATE,
//这样可以确保以后申请锁和释放锁时,都可以唤醒节点
else if (ws == 0 &&
!compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
continue;
}
if (h == head) //如果头结点没有发生变化,则退出
break;
}
}
共享模式下的申请和释放锁与独占模式不同,独占模式一次只允许一个线程申请到锁,而共享模式一次可以允许多个线程申请到锁,所以在共享模式下,当等待队列中的节点申请到锁和释放锁时都会尝试唤醒后继节点,而且这个唤醒过程还可以在等待队列上传播。比如共享模式下,现在有锁5把,线程1一次性申请了5把,线程2和线程3分别要申请2把和3把,因为资源不够,后面两个线程进入等待队列,当线程1释放了锁,会唤醒线程2,线程2唤醒后因为锁还有剩余线程3也会被唤醒。
上面介绍的这些申请锁的方法都是屏蔽线程中断的,也就是在申请锁的过程中不会响应线程中断,下面介绍两个可以响应线程中断的方法。
6、acquireInterruptibly()/acquireSharedInterruptibly()
acquireInterruptibly()用于独占模式,acquireSharedInterruptibly()用于共享模式,它们的逻辑与上面介绍过的代码逻辑类似,只不过是多了对线程中断的判断,下面先看一下acquireInterruptibly():
public final void acquireInterruptibly(int arg)
throws InterruptedException {
if (Thread.interrupted())//判断是否已经中断,中断则抛出异常
throw new InterruptedException();
if (!tryAcquire(arg))
doAcquireInterruptibly(arg);
}
private void doAcquireInterruptibly(int arg)
throws InterruptedException {
final Node node = addWaiter(Node.EXCLUSIVE);
boolean failed = true;
try {
for (;;) {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return;
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())//判断是否已经中断,中断则抛出异常
throw new InterruptedException();
}
} finally {
if (failed)
cancelAcquire(node);
}
}
通过对比acquireInterruptibly()和acquire(),发现两个方法的逻辑区别不大,acquire()方法里面不会抛出线程中断的异常,而且在申请锁的过程中线程中断也不会影响锁的申请,acquireInterruptibly()则不同,一旦发现线程中断了,就会抛出InterruptedException()异常。acquireSharedInterruptibly()也是相同的逻辑,不再详细介绍。
参考文章:
https://www.cnblogs.com/waterystone/p/4920797.html