一、什么是AQS
AQS就是类 AbstractQueuedSynchronizer 的简称,AQS是juc包下的一个基类,juc包
下很多工具类都是基于AQS实现了部分内容,如:ReentrantLock、ThreadPoolExecutor
、阻塞队列、CountDownLatch、Semaphore、CyclicBarrier等等都是基于AQS实现的。
首先AQS提供了一个由 volatile 修饰的,且采用CAS更新的int类型变量 state
其次AQS维护了一个双向链表(队列),有head 和 tail,且每一个节点都是Node 对象
1)AQS属性结构如下:
2)AQS属性字段:
public abstract class AbstractQueuedSynchronizer
extends AbstractOwnableSynchronizer
implements java.io.Serializable {
//双向链表头节点
private transient volatile Node head;
//双向链表尾节点
private transient volatile Node tail;
//状态
private volatile int state;
//内部类 Node,表示链表的每一个节点
static final class Node {
/* */
static final Node SHARED = new Node();
/** */
static final Node EXCLUSIVE = null;
//下边是aqs的state的值(状态)常量
/**唯一大于0的节点,表示节点可以取消 */
static final int CANCELLED = 1;
/** 当前线程节点状态,表示当前线程节点是活动的,可以唤醒后置的等待节点 */
static final int SIGNAL = -1;
/** */
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;
}
}
二、AQS原理之互斥锁的加锁和锁的释放
1、互斥锁的加锁的过程
1.1、acquire() 方法
acquire方法是AQS中互斥锁 加锁的入口,在该方法中当前线程通过 tryAcquire() 获取同
步状态(获取锁)失败后,需要将线包装成Node 节点,然后放入AQS队列中
acquire 是一个模版方法,子类只需要实现自己的 tryAcquire(),如此设计的原因是AQS
不知道子类加锁的行为是如何设计的
acquire() 方法 结构如下
1.2、addWaiter() 方法
该方法的作用是将没有抢到 锁资源的当前线程放到AQS队列中去排队;
该方法采用 “全路径+优化前置” 的方式实现快速入队列。
参数:
mode:表示节点模式,值有2种:EXCLUSIVE =独占的节点(互斥锁),
SHARED =共享的节点(共享锁)
addWaiter() 结构如下:
1.3、end(Node node) 方法
该方法作用是把 当前线程节点添加到AQS队列中,每次都把新创建的Node节点添加到
队尾;
end() 方法结构如下:
private Node enq(final Node node) {
/**
* 向队列中添加节点,每次添加节点都是把新节点添加到尾部
* todo 问题:添加队列时为什么要使用循环?
* 并发条件下,线程A,B 同时调用 enq() 方法,当B线程执行到 compareAndSetTail(t, node)之前,
* 线程 A 正好执行完 t = tail; 这时队列尾节点 tail 还没发生变化,当
* B 线程 执行完 compareAndSetTail(t, node) 后,tail 的值被改变了,这时 线程 A 再执行
* compareAndSetTail(t, node) 时,就会失败,需要重新获取队列尾tail 的值,所以这里使用循环
* 可以保证enq() 方法的原子性
*/
for (;;) {
Node t = tail;
//若当前尾节点为null,则说明当前队列为null,所以先初始化队列
if (t == null) { // Must initialize 懒加载时,head和tail分别代表AQS的头和尾,
//使用CAS实现原子化初始化操作,用一个空节点来初始化,此时head和tail都指向这个空节点
if (compareAndSetHead(new Node()))
tail = head;
} else {//当前队列不为null
//将当前节点的前置指向tail
node.prev = t;
/**
* compareAndSetTail(t, node): 这一步将CAS更新尾节点,设置尾节点tail 指向 node
*/
if (compareAndSetTail(t, node)) {
//添加节点
t.next = node;
return t;
}
//compareAndSetTail(t, node) 若执行失败,则进入下一次循环重新获取 tail 的值
}
}
}
1.4、acquireQueued(final Node node, int arg) 方法
该方法的作用是:线程节点node已经存在于AQS队列中,调用该方法判断 是否将node关联
的线程阻塞
注意一个情况:当加入node加入阻塞队列后,若当前锁是无锁时,该怎么办?
这种情况,那么当前线程一定会尝试获取锁
在该方法中可以发现,AQS队列的头节点是获取锁的节点,即活动的节点
acquireQueued方法结构如下:
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
//标记线程是否中断,中断标志
boolean interrupted = false;
//死循环,这是一个 “自旋” 过程,每个节点(或者说每个线程)都在自省地观察,
// 当条件满足,获取到了同步状态(即获得了锁),就可以从自旋过程退出,否则依旧留在这个自旋过程中(并会阻塞节点的线程)
// 节点之间在循环检查的过程中基本不通信,而是简单地判断自己的前驱结点是否为头节点,这样就使得节点的释放符合FIFO
for (;;) {
//获取node 的前置节点
final Node p = node.predecessor();
/*
* 若node 的前置节点为头节点,且当前线程显式的获取锁成功(即成功获取同步状态),
* 则将节点node 设置为头节点,并退出循环(即退出自旋)
*
* 若p是头节点,有可能此时头节点释放了锁,那么节点p尝试调用tryAcquire 方法去竞争了一次锁
*
*/
if (p == head && tryAcquire(arg)) {//这一段表明:头节点是获取了锁的的节点
//若p获取锁成功,则更新头节点,并释放p的引用
//将node 设置为头节点
setHead(node);
//删除原先的头节点
p.next = null; // help GC
failed = false;
return interrupted;
}
//如果p(node的前置节点)节点不是头节点或node竞争锁失败,那就先判断是否应该阻塞,若需要阻塞,则调用 parkAndCheckInterrupt 来阻塞当前线程
//节点node获取锁(获取同步状态失败)失败,且node中的线程线程已经阻塞和被中断
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
/*
* todo 从上面可以看出,只有前置节点是头节点的节点node(也可以说是线程)才能获得同步状态,这是为什么?
* 1)头节点是已经获得同步状态的节点,头节点的线程在释放了同步状态之后,会唤醒后序的节点(线程),
* 后继节点被唤醒后也要检查自己的前置节点是否是头节点
* 2)维持队列FIFO 的特性
*
* 当前线程是头节点的下一节点所关联的线程
*
* todo 注意:
* 若shouldParkAfterFailedAcquire(Node, Node)方法返回值为false,则acquireQueued(Node, int)方法为死循环,
* 所以acquireQueued(Node, int)方法会一直检查更新节点的状态值,直到当前节点的前驱节点状态值为SIGNAL,这是AQS约定的,
* 只有前继节点的waitStatus是SIGNAL,当前节点才可以安心的去阻塞。因为前继节点的waitStatus是SIGNAL,
* 就相当于当前节点告诉了它的前继节点,我将要去阻塞了,到时候请唤醒我。此时,shouldParkAfterFailedAcquire(Node, Node)方
* 法返回值为true
*
*
*/
}
} finally {
if (failed)
//取消尝试获取锁的节点
cancelAcquire(node);
}
}
1.5、shouldParkAfterFailedAcquire(Node pred, Node node) 方法
该方法用来判断当前线程节点node是否应该阻塞,无非是想找到一个可靠(活着)的节点
将其设置为SIGNAL,然后将当前线程节点node作为其的后置节点。
shouldParkAfterFailedAcquire 方法结构如下:
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
//获取前置节点pred 的waitStatus 的状态值
int ws = pred.waitStatus;
//若前置节点pred的状态是 SIGNAL,表明该前置节点是活动的,但其后继节点则此时可以安全的睡眠,
// 表示后序节点node 的线程需要阻塞,即当前线程应该被阻塞,返回true
if (ws == Node.SIGNAL)
/*
* This node has already set status asking a release
* to signal it, so it can safely park.
*
* 这个节点已经设置状态要求释放(即其所关联的线程需要运行),所以它可以安全阻塞,也就是park。
*/
return true;
if (ws > 0) {//大于0的状态只有一个,即 CANCELLED
//wsws = Node.CANCELLED,即node的前置节点是一个无效的节点,
// 那么则跳过节点pred,查找未被阻塞和取消中断的节点(线程)
/*
* 从后往前查找
* 如果节点 pred 处于取消状态,则跳过取消的节点,向前查找,直到node
* 的前置节点pred 的 waitStatus 的值不大于0为止,并更新node的前置节点
*/
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
//找到距离最近,未被取消的节点作为当前节点node的前置节点,并更新该节点的后置节点为node
pred.next = node;
} else {
/*
* 若前置节点是正常节点,通过CAS将前置节点的状态修改为SIGNAL
*
* waitStatus的值为0或PROPAGATE,但当前节点需要一个SIGNAL 信号,此时还不能阻塞线程;
* 将前置节点的状态值通过CAS 更新未 SIGNAL
*/
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);//设置 pred 的waitStatus 的值为-1
}
return false;
}
1.6、parkAndCheckInterrupt()
该方法表示中断当前线程,
在该方法中 this指代当前AQS对象,表示当前线程阻塞在那个对象上,后期
可以通过jstack来看到,用于排查问题
parkAndCheckInterrupt 方法结构如下:
1.7、cancelAcquire(Node node) 方法
该方法作用是 取消AQS阻塞队列中的节点node,使node后续不再进锁资源的竞争
cancelAcquire 方法结构如下:
private void cancelAcquire(Node node) {
// 节点为null,直接结束
if (node == null)
return;
node.thread = null;//设置节点node 关联的线程为null
// Skip cancelled predecessors
Node pred = node.prev;//获取当前节点的前置节点
//若pred 的节点状态 waitStatus > 0,表示若pred节点也是取消状态,则跳过pred,让node的前置节点指向pred 的上一节点
while (pred.waitStatus > 0)
node.prev = pred = pred.prev;
Node predNext = pred.next;
node.waitStatus = Node.CANCELLED;//设置node为取消状态
// 如果 node是尾节点,且删除node自己,并将node的前置节点设置尾tail操作成功,
// 然后将node前置节点 pred 的后置节点设置为null
if (node == tail && compareAndSetTail(node, pred)) {
//CAS 将指定节点的后置节点设置为null
compareAndSetNext(pred, predNext, null);
} else {
/**
* 如果 node 不是尾节点或将node的后置节点设置尾节点操作失败,
* 则后置节点pred 需要唤醒,尝试获取 pred 的下一链接
*/
int ws;
/**
* 如果 pred 不是头节点,且(节点 pred的状态标记 waitStatus=-1,或将 waitStatus 的值设置为-1 操作成功)
* 且 节点pred 关联的线程不为null,则将 pred 的后置节点设置为 node 的下一节点
*/
if (pred != head &&
((ws = pred.waitStatus) == Node.SIGNAL ||
(ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) &&
pred.thread != null) {
Node next = node.next;
if (next != null && next.waitStatus <= 0)
//将 pred 的后置节点设置为 node 的下一节点
compareAndSetNext(pred, predNext, next);
} else {
//唤醒node节点
unparkSuccessor(node);
}
//node下一节点指向它自己,这时node是游离的状态,没有节点指向它,它也不指向其他节点,它关联的线程也是null,
//此时的node可以被GC回收
node.next = node; // help GC
}
}
2、互斥锁的释放过程
2.1、release() 方法
release方法是AQS中互斥锁 释放锁的入口,在该方法中通过调用子类的tryRelease() 方法
释放锁成功后,则调用unparkSuccessor() 方法唤醒AQS阻塞队列中下一阻塞的线程节点。
注意:当前活动的节点(即持有锁的节点)一定是AQS阻塞队列中的头节点,这在
上边 acquireQueued 方法中可以发现
release() 方法结构如下:
2.2、unparkSuccessor(Node node) 方法
该方法的作用是唤醒当前节点node的后置节点
注意:当前node节点是活跃节点(即持有锁的节点)且node一定是头节点
unparkSuccessor() 方法结构如下:
private void unparkSuccessor(Node node) {
/*
* 如果状态是消极的(即,可能需要信号),尝试在预期信号中清除。如果这个失败或者状态被等待线程改变,它是OK的。
*/
int ws = node.waitStatus;//获取节点node 的状态(标记)
/**
* 开始唤醒后继节点,当前节点是头节点,且当前节点是活跃的,即节点状态为SIGNAL=-1,然后
* CAS将其设置为0,表示当前节点已经响应了这一次的唤醒操作
*/
if (ws < 0)
//将 node.waitStatus 的值设置为0
compareAndSetWaitStatus(node, ws, 0);
/*
* unpark的线程保存在后续节点中,通常是下一个节点(也可能不是下一个节点)。
* 但如果下一个节点waitStatus的值表示取消或明显为空,从尾部向后遍历,以找到实际的
* 未取消的后续节点。
*
* unpark 表示未被占用/使用的线程
*/
/**
* todo 这里是核心:
* 取当前头节点的后继节点作为唤醒节点,但是请注意if中的判断条件
*/
Node s = node.next;//node 节点的后序节点
if (s == null || //这里为什么可能会为null? 因为在enq()入队列方法中先更新的是尾节点tail,然后才更新旧的tail.next,所以node.next可能一瞬间为null
s.waitStatus > 0) {//s节点是无效节点?因为前面的enq()入队列方法更新无效节点那一段不是原子性的,所以多线程下node.next可能指向的还是无效的节点
s = null;
/**
* todo
* tail节点一定是最新入队列的节点,从队列的尾部向前遍历,
* 找到node节点后面第一个有效的节点(node节点的后继的第一个有效节点)
*/
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
//找到需要唤醒的节点
s = t;
}
//此时s就是要唤醒的节点
if (s != null)
//唤醒节点s 中的线程
LockSupport.unpark(s.thread);
}