作用
如图aqs会在很多地方用到,如之前介绍的CountDownLatch,信号量Semaphore,可重入锁ReentrantLock,线程池中的worker等,都用到了aqs,因此该数据结构在并发编程中十分重要
CountDownLatch,Semaphore,ReentrantLock都用内部声明了Sync类来继承aqs,实现逻辑按照不同的类实现不同的作用
关键词:让线程协作
aqs实现的作用如下:
- 线程状态的原子性管理
- 线程的阻塞与解除阻塞
- 线程队列的管理
比喻:HR 和面试官
数据结构
AQS内部实现了两个队列,一个同步队列,一个条件队列。
- 同步队列:当线程获取资源失败之后,就进入同步队列的尾部保持自旋等待,不断判断自己是否是链表的头节点,如果是头节点,就不断参试获取资源,获取成功后则退出同步队列。
- 条件队列:为Lock实现的一个基础同步器,并且一个线程可能会有多个条件队列,只有在使用了Condition才会存在条件队列。
同步队列和条件队列都是由一个个Node组成的。AQS内部有一个静态内部类Node。
static final class Node {
static final Node EXCLUSIVE = null;
//当前节点由于超时或中断被取消
static final int CANCELLED = 1;
//表示当前节点的前节点被阻塞
static final int SIGNAL = -1;
//当前节点在等待condition
static final int CONDITION = -2;
//状态需要向后传播
static final int PROPAGATE = -3;
volatile int waitStatus;
volatile Node prev;
volatile Node next;
volatile Thread thread;
Node nextWaiter;
final boolean isShared() {
return nextWaiter == SHARED;
}
final Node predecessor() throws NullPointerException {
Node p = prev;
if (p == null)
throw new NullPointerException();
else
return p;
}
Node() { // Used to establish initial head or SHARED marker
}
Node(Thread thread, Node mode) { // Used by addWaiter
this.nextWaiter = mode;
this.thread = thread;
}
Node(Thread thread, int waitStatus) { // Used by Condition
this.waitStatus = waitStatus;
this.thread = thread;
}
}
关键是这几个
前后两个节点,节点上的thread类,还有nextWaiter节点和waitStatus状态
就是如图右下角那几个参数
ConditionObject则实现了条件队列,这个队列可以创建多个
重要方法的源码
获取独占锁
//独占模式下获取资源
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
这里的tryAcquire(arg)
是抽象方法,有AQS的子类来实现,因为每个子类实现的锁是不一样的。一共有四个方法在AQS中没有具体实现:
- tryAcquire(int arg):获取独占锁
- tryRelease(int arg):释放独占锁
- tryAcquireShared(int arg):获取共享锁
- tryReleaseShared(int arg):释放共享锁
最重要的是acquireQueued(addWaiter(Node.EXCLUSIVE), arg)
,源码如下
private Node addWaiter(Node mode) {
Node node = new Node(Thread.currentThread(), mode);
// Try the fast path of enq; backup to full enq on failure
Node pred = tail;
//尝试快速入队
if (pred != null) { //队列已经初始化
node.prev = pred;
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node; //快速入队成功后,就直接返回了
}
}
//快速入队失败,也就是说队列都还每初始化,就使用enq
enq(node);
return node;
}
//执行入队
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;//打断循环
}
}
}
}
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor();//拿到node的上一个节点
//前置节点为head,说明可以尝试获取资源。排队成功后,尝试拿锁
if (p == head && tryAcquire(arg)) {
setHead(node);//获取成功,更新head节点
p.next = null; // help GC
failed = false;
return interrupted;
}
//尝试拿锁失败后,根据条件进行park
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
//获取资源失败后,检测并更新等待状态
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;
if (ws == Node.SIGNAL)
/*
* This node has already set status asking a release
* to signal it, so it can safely park.
*/
return true;
if (ws > 0) {
/*
* Predecessor was cancelled. Skip over predecessors and
* indicate retry.
*/
do {
//如果前节点取消了,那就往前找到一个等待状态的接待你,并排在它的后面
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
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.
*/
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
//阻塞当前线程,返回中断状态
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);
return Thread.interrupted();
}
park
在AQS的实现中有一个出现了一个park的概念。park即LockSupport.park()
.它的作用是阻塞当前线程,并且可以调用LockSupport.unpark(Thread)
去停止阻塞。它们的实质都是通过UnSafe类使用了CPU的原语。
在AQS中使用park的主要作用是,让排队的线程阻塞掉(停止其自旋,自旋会消耗CPU资源),并在需要的时候,可以方便的唤醒阻塞掉的线程。
入队
上面的代码可以看到,获取锁失败后,会先执行addWaiter方法加入队列,然后执行acquireQueued方法自旋地获取锁直到成功。
addWaiter代码逻辑如下图,简单说就是把node入队,入队后返回node参数给acquireQueued方法:
这里有一个点需要注意,如果队列为空,则新建一个Node作为队头。
入队后获取锁
acquireQueued自旋获取锁逻辑如下图:
这里有几个细节:
1.waitStatus
- CANCELLED(1):当前节点取消获取锁。当等待超时或被中断(响应中断),会触发变更为此状态,进入该状态后节点状态不再变化。
- SIGNAL(-1):后面节点等待当前节点唤醒。
- CONDITION(-2):Condition中使用,当前线程阻塞在Condition,如果其他线程调用了Condition的signal方法,这个结点将从等待队列转移到同步队列队尾,等待获取同步锁。
- PROPAGATE(-3):共享模式,前置节点唤醒后面节点后,唤醒操作无条件传播下去。
- 0:中间状态,当前节点后面的节点已经唤醒,但是当前节点线程还没有执行完成。
2.获取锁失败后挂起
如果前置节点不是头节点,或者前置节点是头节点但当前节点获取锁失败,这时当前节点需要挂起,分三种情况,
前置节点waitStatus=-1,如下图:
前置节点waitStatus > 0,如下图:
前置节点waitStatus < 0 但不等于 -1,如下图:
3.取消获取锁
如果获取锁抛出异常,则取消获取锁,如果当前节点是tail节点,分两种情况如下图:
如果当前节点不是tail节点,也分两种情况,如下图:
4.对中断状态忽略
5.如果前置节点的状态是 0 或 PROPAGATE,会被当前节点自旋过程中更新成-1,以便之后通知当前节点。
参考
AQS 的作用和重要性是什么?
https://blog.csdn.net/vincent_wen0766/article/details/108718349
AQS 模型(源码内的其他方法可以看这一篇)
https://www.yisu.com/zixun/457731.html
AQS 数据结构和原理(获取独占锁自定义公平和非公平锁)
https://www.cnblogs.com/zofun/p/12206759.html
AQS 同步队列和条件队列
https://www.cnblogs.com/liqiangchn/p/12081838.html