AQS源码学习
AQS(全称AbstractQueuedSynchronizer抽象队列同步器)作为juc包下重要的基础类,是实现其他并发工具的基础类,来学习下AQS源码
1、类继承结构
先看看类继承结构,AQS继承了AbstractOwnableSynchronizer这个类
1.1、AbstractOwnableSynchronizer类的结构如下
public abstract class AbstractOwnableSynchronizer
implements java.io.Serializable {
private static final long serialVersionUID = 3737899427754241961L;
protected AbstractOwnableSynchronizer() { }
// 独占模式同步下的当前拥有者线程
private transient Thread exclusiveOwnerThread;
// 设置独占模式下资源拥有者线程
protected final void setExclusiveOwnerThread(Thread thread) {
exclusiveOwnerThread = thread;
}
// 获取独占模式下资源拥有着线程
protected final Thread getExclusiveOwnerThread() {
return exclusiveOwnerThread;
}
}
2、AQS的模板方法
好了,AQS的父类就说到这里,继续来分析AQS。“同步器的设计是基于模板方法模式的,也就是说,使用者需要继承同步器并重写指定的 方法,随后将同步器组合在自定义同步组件的实现中,并调用同步器提供的模板方法,而这些 模板方法将会调用使用者重写的方法。”-引用自《java并发编程的艺术》 说白了要明白,AQS已经写好了一些方法(模板方法),这些是通用逻辑如:
2.1、 AQS使用者可能重写的方法
总共9个模板方法,本文只分析红框内的4个模板方法,其他方法分析与这4个类似。除此之外在使用AQS的时候,按照情况需要自行重写以下方法中的一些:
独占式表示同一时间同步资源只能被一个线程占有,这样在多线程场景下会降低效率;共享式表示同一时间同步资源可以被多个线程占有,这样多线程下会提升效率。注意到tryAcquireShared方法返回的数据大于等于0表示获取成功,大于0表示当前线程获取成功并且还有剩余资源,其他线程还可获取;等于0表示当前线程获取成功,但是没有剩余资源。在重写这个方法的时候要注意这个语义。
3、AQS的field属性:
static final class Node {}
// 同步队列的头节点,头节点是获取了同步资源的节点
private transient volatile Node head;
// 同步队列的尾节点
private transient volatile Node tail;
// 同步资源
private volatile int state;
3.1、Node节点是一个静态内部类:
// 内部持有一个静态内部类Node,该类设计了等待线程的数据结构和状态
static final class Node {
// 共享模式的节点
static final Node SHARED = new Node();
// 标记表明是在独占模式下等待
static final Node EXCLUSIVE = null;
// waitStatus值,指示线程已取消
static final int CANCELLED = 1;
// waitStatus值,当前线程在释放同步状态时需要唤醒后继节点
static final int SIGNAL = -1;
// waitStatus值,指示线程正在等待条件
static final int CONDITION = -2;
// waitStatus值,指示下一个acquireShared应该无条件传播
static final int PROPAGATE = -3;
// 等待状态
volatile int waitStatus;
// 前驱
volatile Node prev;
// 后继
volatile Node next;
// 等待节点的线程
volatile Thread thread;
// Node既可以作为同步队列节点使用,也可以作为Condition的等待队列节点使用。
// 在作为同步队列节点时,nextWaiter可能有两个值:EXCLUSIVE、SHARED;
// 在作为等待队列节点使用时,nextWaiter保存后继节点。
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;
}
}
4、AQS模板方法分析
4.1、独占式获取锁acquire(int arg)源码
来分析独占式获取锁资源代码acquire()
public final void acquire(int arg) {
// 先调用tryAcquire获取资源,如果获取成功则返回,如果获取失败则构造等待队列的
// 节点,即addWaiter(),然后调用acquireQueued()方法获取资源
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
// 获取过程中有终端,补上中断状态
selfInterrupt();
}
4.1.1、addWaiter(Node mode)源码
// 分析下addWaiter()方法
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,来看下这个方法
enq(node);
return node;
}
4.1.1.1、enq(final Node node)源码
private Node enq(final Node node) {
//(喔,又是死循环,李大爷写死循环有点东西的)在死循环里面,当前线程一直尝试
// cas排队尾,这就是经典的锁自旋
for (;;) {
Node t = tail;
// tail为null,说明当前线程是第一个,所以要cas设置下队列头尾节点
if (t == null) { // Must initialize
if (compareAndSetHead(new Node()))
tail = head;
} else {
node.prev = t;
// cas设置到队尾
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
4.1.2、acquireQueued(final Node node, int arg)源码
// 好了,到这里addWaiter执行完毕,终于将当前线程放到了等待队列队尾,接下来看下acquireQueued这个方法
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
// cas尝试获取资源,如果获取成功就返回,否则可能要park,待当前线程被唤醒后
// 继续cas自旋获取
for (;;) {
// 获取到当前节点的前驱
final Node p = node.predecessor();
// 当前节点刚好是获取到资源的头节点并且当前线程也获取资源成功,则完成
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
// 当前节点的前驱不是头节点,或者获取资源失败。判断下是否要park
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
4.1.2.1、shouldParkAfterFailedAcquire(Node pred, Node node)源码
// 看下shouldParkAfterFailedAcquire这个方法
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
// 拿到前驱节点的状态
int ws = pred.waitStatus;
// 前驱节点已经设置好了,就可以放心去park了
if (ws == Node.SIGNAL)
return true;
// 大于0表示当前节点的前驱节点放弃获取同步资源,需要循环往前面找没有放弃的
// 最近的一个节点
if (ws > 0) {
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
// 尝试将前驱节点的状态设置为SIGNAL,无论成功与否,下次进来再观察
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
// 走到这里表示还不可以park
return false;
}
4.1.2.2、parkAndCheckInterrupt()源码
// 最后看下真正的park方法parkAndCheckInterrupt
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);
return Thread.interrupted();
}
// 到此位置独占式获取同步资源的代码分析完了
独占式抢占锁流程:
- 尝试先抢占一次,抢占成功返回;
- 抢占失败,当前线程包装成节点加入等待队列末尾,cas自旋加入队尾;
- 加入队尾成功,尝试获取锁资源,获取成功,设置未头节点,返回;
- 获取失败,cas判断是否需要休眠等待,只有当前节点的前驱节点状态是SIGNAL,才可以等待;
- 前驱节点是SIGNAL,当前线程park下。
4.2、独占式释放锁release(int arg)源码
来分析独占式释放锁的源码release()
public final boolean release(int arg) {
// 如果释放成功
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
// 唤醒后继等待节点
unparkSuccessor(h);
return true;
}
return false;
}
unparkSuccessor(Node node)源码
private void unparkSuccessor(Node node) {
// 当前线程节点的状态
int ws = node.waitStatus;
// ws<0 是正常的节点状态
if (ws < 0)
// cas设置节点状态,这里会设置成功,因为是独占式的
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);
}
// 独占式释放锁的代码还是比较简单,就看到这里
4.3、共享式获取锁acquireShared(int arg)
再来分析共享式获取同步资源的代码
public final void acquireShared(int arg) {
// 这里尝试共享式获取同步资源,注意不同的是只要返回值>=0就表示获取成功
// 因为tryAcquireShared由子类去具体实现,如果获取失败,我们重点看下
// doAcquireShared这个方法
if (tryAcquireShared(arg) < 0)
doAcquireShared(arg);
}
4.3.1、doAcquireShared(int arg)源码
private void doAcquireShared(int arg) {
// 这里和独占式的addWaiter方法是一样的,都是cas插入队列的末尾,这里就不再赘述
final Node node = addWaiter(Node.SHARED);
boolean failed = true;
try {
boolean interrupted = false;
// 这里进行自旋获取同步资源
for (;;) {
// 获取节点的前驱
final Node p = node.predecessor();
// 前驱是头节点,那么就可以尝试获取同步资源
if (p == head) {
// 尝试获取资源
int r = tryAcquireShared(arg);
// r>=0 获取同步资源成功了
if (r >= 0) {
// 设置头并且将消息广播给后继节点,让后继节点也去获取资源
setHeadAndPropagate(node, r);
p.next = null; // help GC
if (interrupted)
selfInterrupt();
failed = false;
return;
}
}
// 这里和独占式获取失败逻辑相同,不再分析
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
// 如果获取失败了,取消获取同步资源
if (failed)
cancelAcquire(node);
}
}
4.3.1.1、setHeadAndPropagate(Node node, int propagate)源码
// 分析下setHeadAndPropagate这个方法
private void setHeadAndPropagate(Node node, int propagate) {
Node h = head; // Record old head for check below
// 设置头
setHead(node);
// 如果还有资源,广播给后继节点
if (propagate > 0 || h == null || h.waitStatus < 0 ||
(h = head) == null || h.waitStatus < 0) {
Node s = node.next;
if (s == null || s.isShared())
// 共享锁释放
doReleaseShared();
}
}
4.3.1.1.1、doReleaseShared()源码
// 看下doReleaseShared源码
private void doReleaseShared() {
for (;;) {
Node h = head;
if (h != null && h != tail) {
int ws = h.waitStatus;
if (ws == Node.SIGNAL) {
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
continue; // loop to recheck cases
// 唤醒后继节点
unparkSuccessor(h);
}
else if (ws == 0 &&!compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
continue; // loop on failed CAS
}
if (h == head) // loop if head changed
break;
}
}
// 分析下unparkSuccessor源码
private void unparkSuccessor(Node node) {
// 节点状态
int ws = node.waitStatus;
if (ws < 0)
// 当前节点状态设置为0
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);
}
4.4、共享式释放锁releaseShared(int arg)
分析下共享锁释放锁源码
public final boolean releaseShared(int arg) {
// 尝试释放资源,如果释放成功,则唤醒后续的节点
if (tryReleaseShared(arg)) {
// 上面已经分析过了,简单来说就是唤醒后继的一个正常的节点
doReleaseShared();
return true;
}
return false;
}
至此,AQS的独占式获取、释放资源;共享式获取、释放资源源码都分析完了,有了这些认识,后续在分析其他锁的时候就会简单许多。当然AQS还有其他一些重要方法没有分析到,后续有机会继续看下。
5、AQS中的Condition
Condition 中的node状态是CONDITION = -2,等待线程,在等待队列里面,当被唤醒时候,进入到
同步队列里面;在同步队列里面调用await,会进入等待队列。