AbstractQueuedSynchronizer,简称AQS,JUC并发包中常用的ReentrantLock, CountDownLatch等都依赖AQS。子类通过继承AQS并实现它的抽象方法来管理同步状态,它简化了锁的实现方式,屏蔽了同步状态管理、线程的排队、等待与唤醒等底层操作,但是通过AQS实现的功能却是不同的。
下图就是AQS的数据模型:
接下来再来看看AbstractQueuedSynchronizer的成员变量
// CLH队列头结点
private transient volatile Node head;
// CLH队列尾结点
private transient volatile Node tail;
// 标识锁的状态:0:锁空闲;>1:锁被占用,大于1标识被重入的次数
private volatile int state;
// 继承AbstractOwnableSynchronizer的属性,表示当前持有锁的线程
private transient Thread exclusiveOwnerThread;
AQS中state状态的变更是基于CAS实现的,state状态通过volatile保证共享变量的可见性,再由CAS 对该同步状态进行原子操作,从而保证原子性和可见性。
protected final boolean compareAndSetState(int expect, int update) {
// unsafe的CAS操作-CPU的原子指令
return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}
内部类:Node
static final class Node {
// 共享模式下等待的标记
static final Node SHARED = new Node();
// 独占模式下等待的标记
static final Node EXCLUSIVE = null;
// 线程的等待状态 表示线程已经被取消
static final int CANCELLED = 1;
// 线程的等待状态 表示后继线程需要被唤醒
static final int SIGNAL = -1;
// 线程的等待状态 表示线程在Condtion上
static final int CONDITION = -2;
// 表示下一个acquireShared需要无条件的传播
static final int PROPAGATE = -3;
/**
* SIGNAL: 当前节点的后继节点处于等待状态时,如果当前节点的同步状态被释放或者取消,
* 必须唤起它的后继节点
*
* CANCELLED: 一个节点由于超时或者中断需要在CLH队列中取消等待状态,被取消的节点不会再次等待
*
* CONDITION: 当前节点在等待队列中,只有当节点的状态设为0的时候该节点才会被转移到同步队列
*
* PROPAGATE: 下一次的共享模式同步状态的获取将会无条件的传播
* waitStatus的初始值时0,使用CAS来修改节点的状态
*/
volatile int waitStatus;
/**
* 当前节点的前驱节点,当前线程依赖它来检查waitStatus,在入队的时候才被分配,
* 并且只在出队的时候才被取消(为了GC),头节点永远不会被取消,一个节点成为头节点
* 仅仅是成功获取到锁的结果,一个被取消的线程永远也不会获取到锁,线程只取消自身,
* 而不涉及其他节点
*/
volatile Node prev;
/**
* 当前节点的后继节点,当前线程释放的才被唤起,在入队时分配,在绕过被取消的前驱节点
* 时调整,在出队列的时候取消(为了GC)
* 如果一个节点的next为空,我们可以从尾部扫描它的prev,双重检查
* 被取消节点的next设置为指向节点本身而不是null,为了isOnSyncQueue更容易操作
*/
volatile Node next;
/**
* 当前节点的线程,初始化后使用,在使用后失效
*/
volatile Thread thread;
/**
* 链接到下一个节点的等待条件,或特殊的值SHARED,因为条件队列只有在独占模式时才能被访问,
* 所以我们只需要一个简单的连接队列在等待的时候保存节点,然后把它们转移到队列中重新获取
* 因为条件只能是独占性的,我们通过使用特殊的值来表示共享模式
*/
Node nextWaiter;
/**
* 如果节点处于共享模式下等待直接返回true
*/
final boolean isShared() {
return nextWaiter == SHARED;
}
/**
* 返回当前节点的前驱节点,如果为空,直接抛出空指针异常
*/
final Node predecessor() throws NullPointerException {
Node p = prev;
if (p == null)
throw new NullPointerException();
else
return p;
}
Node() { // 用来建立初始化的head 或 SHARED的标记
}
Node(Thread thread, Node mode) { // 指定线程和模式的构造方法
this.nextWaiter = mode;
this.thread = thread;
}
Node(Thread thread, int waitStatus) { // 指定线程和节点状态的构造方法
this.waitStatus = waitStatus;
this.thread = thread;
}
}
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
acquire()是独占式获取同步状态,这个方法分别调用了下面4个方法
protected boolean tryAcquire(int arg) {
throw new UnsupportedOperationException();
}
AQS并没有实现这个方法,具体的实现由它的继承类进行重写,如ReentrantLock的Sync类等,很明显,这是个模板方法模式!
/**
* 把Node节点添加到同步队列的尾部
*/
private Node addWaiter(Node mode) {
Node node = new Node(Thread.currentThread(), mode); // 以独占模式把当前线程封装成一个Node节点
// 尝试快速插入尾部
Node pred = tail; // 当前队列的尾节点赋给pred
if (pred != null) { // 先觉条件 尾节点不为空
node.prev = pred; // 把pred作为node的前继节点
if (compareAndSetTail(pred, node)) { //利用CAS把node作为尾节点
pred.next = node; // 把node作为pred的后继节点
return node; // 直接返回node
}
}
// 上一步快速插入尾部失败则通过enq自旋的方式把node插入到队列中。
enq(node);
return node;
}
/**
* 采用自旋的方式把node插入到队列中
*/
private Node enq(final Node node) {
for (;;) {
Node t = tail;
if (t == null) { // 如果尾结点为空,说明队列为空,创建一个空的标志结点作为head结点,并将tail也指向它。
if (compareAndSetHead(new Node())) // 新建一个节点利用CAS设为头节点,就是这样的形式 head=tail=null
tail = head;
} else { // 正常流程,放入队尾
node.prev = t; // 把t设为node的前驱节点
if (compareAndSetTail(t, node)) { // 利用CAS把node节点设为尾节点
t.next = node; // 更改指针 把node作为t的后继节点
return t; // 直接返回t
}
}
}
}
尝试快速插入尾部的代码和enq(final Node node)方法中的代码有重复,之所以有这部分“重复代码”,是因为对某些特殊情况进行提前处理,牺牲一定的代码可读性换取性能提升。
/*
* 此主要是通过自旋方式获取同步状态
*/
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false; // 默认线程没有被中断过
for (;;) {
final Node p = node.predecessor(); // 获取该节点的前驱节点p
if (p == head && tryAcquire(arg)) { // 如果p是头节点并且能获取到同步状态
setHead(node); // 把当前节点设为头节点,这样就能把当前节点给移除
p.next = null; // 把p的next设为null,便于GC
failed = false; // 标志--表示成功获取同步状态,默认是true,表示失败
return interrupted; // 返回该线程在获取到同步状态的过程中有没有被中断过
}
// 上一步尝试获取同步状态失败,则在队列中挂起当前线程
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed) // 如果fail为true,直接移除当前节点
cancelAcquire(node);
}
}
/*
* 用于判断是否挂起当前线程
*/
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;//拿到前驱的状态
if (ws == Node.SIGNAL)
//如果已经告诉前驱拿完号后通知自己一下,那就可以安心休息了
return true;
if (ws > 0) {
/*
* 如果前驱放弃了,那就一直往前找,直到找到最近一个正常等待的状态,并排在它的后边。
* 注意:那些放弃的结点,由于被自己“加塞”到它们前边,它们相当于形成一个无引用链,稍后就会被保安大叔赶走了(GC回收)!
*/
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
//如果前驱正常,那就把前驱的状态设置成SIGNAL,告诉它拿完号后通知自己一下。有可能失败,人家说不定刚刚释放完呢!
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);//调用park()使线程进入waiting状态(线程此时阻塞在这里)
return Thread.interrupted();//如果被唤醒,查看自己是不是被中断的。(注意此方法被唤醒后才会执行)
}
因为线程在进入CLH队列是通过调用LockSupport.park进入阻塞状态的,外部中断了该线程是不会立即中断的,只会修改Thread内部的中断状态值,不会抛出中断异常。直到被唤醒后,可以调用Thread.interrupted()方法查看阻塞过程中是否被中断过,然后再自我中断。
/**
* 当前线程的自我中断
*/
private static void selfInterrupt() {
Thread.currentThread().interrupt();
}
selfInterrupt 执行的前提是 acquireQueued(addWaiter(Node.EXCLUSIVE), arg)方法返回 true。这个方法返回的是线程在获取锁的过程中是否发生过中断,返回 true 则证明发生过中断。所以 acquire 中的 selfInterrupt 其实是对获取锁的过程中发生过的中断的补充。
为什么不直接用 isInterrupt()判断?是因为在获取锁的过程中,是通过 park+ 死循环实现的。每次 park 被唤醒之后都会重置中断状态,所以拿到锁的时候中断状态都是被重置后的。
最后总结下独占式同步状态获取流程,也就是acquire(int arg)方法调用流程
public final boolean release(int arg) {
// 先尝试释放同步状态
if (tryRelease(arg)) {
Node h = head;
// 如果头结点不为空,且状态是-1SIGNAL,则唤醒下一个阻塞的节点
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
release()是独占式释放同步状态,这个方法分别调用了下面2个方法
protected boolean tryRelease(int arg) {
throw new UnsupportedOperationException();
}
同上面的tryAcquire()方法一样,AQS并没有实现这个方法,具体的实现由它的继承类进行重写,如ReentrantLock的Sync类等,很明显,这也是个模板方法模式!
private void unparkSuccessor(Node node) {
// 注意:这里的node是head节点
int ws = node.waitStatus;
// 先将head节点的waitStatus设置为0
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
Node s = node.next;
// 如果head节点的后继节点为null或者被取消(waitStatus=1),
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);
}
上述代码中需要注意的是,在找最靠前的一个需要被唤醒的节点时,如果head节点的后继节点为null,则从尾结点开始往前找最靠前的一个有效节点,至于为什么是从后往前找,是因为前面把Node节点添加到同步队列的尾部时候,enq(final Node node)是通过compareAndSetTail(t, node)这个CAS操作的,但是当CAS操作成功(tail指向当前node),执行if代码块并不是原子操作,这个时候,node与前一个节点t之间,node的prev指针在CAS操作之前已经建立,而t的next指针还未建立,此时若其他线程调用了release()操作,寻找需要唤醒的下一个节点,从头开始找就无法遍历完整的队列,而从后往前找就可以。
注意:CLH队列是双向链表,也就是说2个节点要想建立链接,需要设置2个指针,比如t, node两个节点,只有当node.prev = t; t.next = node; 这两个指针都设置成功才能建立起链接,否则就会断掉。
private Node enq(final Node node) {
for (;;) {
Node t = tail;
if (t == null) { // 如果t为空,说明队列为空,必须初始化
if (compareAndSetHead(new Node())) // 新建一个节点利用CAS设为头节点,就是这样的形式 head=tail=null
tail = head;
} else { // 尾节点不为空的情况
// 把t设为node的前驱节点在CAS操作之前建立
node.prev = t;
if (compareAndSetTail(t, node)) { // 利用CAS把node节点设为尾节点
// 把node作为t的后继节点在CAS操作之后才建立,
// 在执行此方法之前,若其他线程调用了release()操作,寻找需要唤醒的下一个节点,从头开始找就无法遍历完整的队列,而从后往前找就可以。
// 所以在释放锁之后寻找需要唤醒的下一个节点,需要从后往前找
t.next = node;
return t; // 直接返回t
}
}
}
}