AQS AbstractQueuedSynchronizer
AQS提供了一种框架,以使用FIFO的同步队列实现阻塞锁
AQS成员变量
// 同步队列的头。
// 公平的锁先入先出。
private transient volatile Node head;
// 等待队列的尾
private transient volatile Node tail;
// 同步器的状态,根据当前状态进行判断是否可以获得当前锁
// 如果当前state是0,那么可以获得锁
// 可重入锁,每次获得锁+1,每次释放锁-1
private volatile int state;
Node 内部类
Node被static final
修饰,无法被继承
static final class Node {
/**
* 同步队列单独的属性
*/
//node 是共享模式
static final Node SHARED = new Node();
//node 是排他模式
static final Node EXCLUSIVE = null;
// 当前节点的前节点
// 节点被 acquire 后就会变成head
// head 节点不能被 cancelled
volatile Node prev;
// 当前节点的下一个节点
volatile Node next;
/**
* 两个队列共享的属性
*/
// 表示当前节点的状态,通过节点的状态来控制节点的行为
// 普通同步节点,就是 0 ,条件节点是 CONDITION -2
volatile int waitStatus;
// waitStatus 的状态有以下几种
// 线程被取消
static final int CANCELLED = 1;
// 状态的意义:同步队列中的节点在自旋获取锁的时候,如果前一个节点的状态是 SIGNAL,那么自己就可以阻塞休息了,否则自己一直自旋尝试获得锁
// 该结点的后继结点线程需要阻塞
static final int SIGNAL = -1;
// 表示当前 node 正在条件队列中,当有节点从同步队列转移到条件队列时,就会被赋值成 CONDITION
static final int CONDITION = -2;
// 无条件传播,共享模式下,该状态的进程处于可运行状态
static final int PROPAGATE = -3;
// 当前节点的线程
volatile Thread thread;
// 在同步队列中,nextWaiter 并不真的是指向其下一个节点,我们用 next 表示同步队列的下一个节点,这只是表示当前 Node 是排他模式还是共享模式
// 但在条件队列中,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;
}
}
内部类Node
主要包含的对象
volatile Thread thread
线程对象volatile Node prev
前向结点volatile Node next
后继结点volatile int waitStatus
线程在等待队列中的状态
获取锁
AQS提供两个方法获取锁
tryAcquire()
尝试获取锁(修改标志位),并立即返回acquire()
获取锁(修改标志位),进入等待队列直到获取锁为止
tryAcquire()
protected boolean tryAcquire(int arg) {
throw new UnsupportedOperationException();
}
- 参数为int,代表对
state
的修改 - 返回值为
boolean
类型,代表是否能成功获得锁 - 该方法被
protected
修饰,没有具体实现,仅仅抛出一个异常,AQS的继承类需要重写Override该方法,否则直接抛出不支持该方法的异常
acquire()
// 排他模式下,尝试获得锁
// 如果执行一次tryAcquire就成功,直接返回,否则线程尝试进入同步队列,tryAcquire 交给子类去实现
// addWaiter 把当前线程放到同步队列的队尾
// acquireQueued 方法两个作用,1:阻塞当前节点,2:节点被唤醒时,使其能够获得锁
// 如果以上步骤都失败了,打断线程
public final void acquire(int arg) {
// tryAcquire方法是需要实现类去实现的,实现思路一般都是 cas 给 stats 赋值来决定是否能获得锁
if (!tryAcquire(arg) &&
// addWaiter 入参代表是排他模式
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
acquire()
的修饰符为public final
,即所有的继承类都直接调用该方法,而且不允许重写(一定能获得锁)- 查看具体执行方法,如果
tryAcquire(arg)
成功获取锁返回true
,则!tryAcquire()
直接跳出判断语句,且不再执行selfInterrupt()
AddWaiter()
// node 追加到同步队列的队尾
// 入参是 Node 的模式(排他模式还是共享模式)
// 出参是新增的 node
// 新 node.pre = 队尾
// 队尾.next = 新 node
private Node addWaiter(Node mode) {
// 初始化 Node
Node node = new Node(Thread.currentThread(), mode);
// 这里的逻辑和 enq 一致,enq 的逻辑仅仅多了队尾是空,初始化的逻辑
// 这个思路在java源码中很常见,先简单的尝试放一下,成功立马返回,如果不行,再while循环
// 很多时候,这种算法可以帮忙解决大部分的问题,发部分的入队可能一次都能成功,无需自旋
Node pred = tail;
if (pred != null) {
node.prev = pred;
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
//自旋保证node加入到队尾
enq(node);
return node;
}
addWaiter
方法将当前线程封装成node
,然后加入等待队列,返回值为当前节点Node node = new Node(...)
先新建节点对象,将节点插入队尾- 获取尾结点的指针,最为当前节点的前置节点
- 尾结点不为空时,用CAS操作将当前节点置为尾结点
- 将前置节点的next指针指向已成为尾结点的当前指针
if (compareAndSetTail(pred, node){...}
方法体中的引用之余pred
、node
有关,如果尾结点tail
发生改变,对该操作也没有影响- 若当前尾结点为空,或者第一次尝试的CAS操作失败时,则会执行完整的队列方法
enq(node)
- 先尝试快速入队,失败后再进行完整入队
enq()
// 线程加入同步队列中方法,追加到队尾
// 这里需要重点注意的是,返回值是添加 node 的前一个节点
private Node enq(final Node node) {
for (;;) {
// 得到队尾节点
Node t = tail;
// 如果队尾为空,说明当前同步队列都没有初始化,进行初始化
// tail = head = new Node();
if (t == null) {
if (compareAndSetHead(new Node()))
tail = head;
//当前节点置为队尾
} else {
node.prev = t;
// node 追加到队尾
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
- 完整的入队方法会对当前队列进行初始化,并自旋通过CAS将当前节点插入,直到入队为止
for (;;)
自旋保证一定能够获得锁- 自旋程序块中有判断当前尾结点是否为空的操作,在循环中会影响开销,因此在
addWaiter
中先执行一次快速入队后,若快速入队不成功再执行完整入队,在保证一定能入队的同时减少函数调用的开销
acquireQueued()
// 主要做两件事情:
// 1:通过不断的自旋尝试使自己前一个节点的状态变成 signal,然后阻塞自己。
// 2:如果前一个节点获得锁,执行完成之后,再释放锁时,会把阻塞的 node 唤醒,唤醒之后再次自旋,再次无限 for 循环尝试获得锁
// 返回false表示获得锁成功,返回true表示失败
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
// 自旋
for (;;) {
// 选上一个节点
final Node p = node.predecessor();
// 有两种情况会走到 if:
// 1:node 之前没有获得锁,进入 acquireQueued 方法时,发现他的前置节点就是头节点,于是尝试获得一次锁
// 2:node 之前一直在阻塞沉睡,然后被唤醒,此时唤醒 node 的节点正是其前置节点,也能走到 if,具体见 release 方法
// 如果自己 tryAcquire 成功,就立马把自己设置成 head,把上一个节点移除
// 如果 tryAcquire 失败,尝试进入同步队列
if (p == head && tryAcquire(arg)) {
// 获得锁,设置成 head 节点
setHead(node);
//p被回收
p.next = null; // help GC
failed = false;
return interrupted;
}
// shouldParkAfterFailedAcquire 把node的前一个节点状态置为SIGNAL
// 只要前一个节点状态是SIGNAL了,那么自己就可以阻塞(park)了
// parkAndCheckInterrupt 阻塞当前线程
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
//如果获得node的锁失败,将node从队列中移除
if (failed)
cancelAcquire(node);
}
}
- 初始化
failed = true
,只有在return
之前,failed = false
finally
代码块中,若failed
为true
,则执行cancelAcquire(node)
方法,该方法将Node
的waitStatus
置为CANCEL
,以及其他一些清理工作- 在
acquireQueued
方法正常执行并且return
时,failed
的最终指永远是false,只有执行抛出异常才会进入finally
代码块中 - 方法的主体为包含在自选操作中,如果当前节点的前置节点是头结点,而且当前线程的获取锁成功了,则达成目的,直接返回即可
- AQS中的FIFO队列中头结点是虚节点,是哨兵节点,第二个节点才是真正获取锁的结点,当第二个节点获取锁之后,这个结点会变为头结点,头结点会出队
- 判断当前节点的前置节点是否为头结点等价于判断当前节点有误资格获取锁
- 没有出队的线程挂起,之后再将其唤醒,避免大量自旋
Java中断补充
Java中断作用于线程对象,并不会促使线程直接挂起,而是根据Thread活动状态判断
- 等待状态:interrupt将会抛出中断异常
- 运行状态:interrupt改变Thread对象中的中断异常值,不影响线程的继续运行
shouldParkAfterFailedAcquire()
该方法通过前置节点的waitStatus
判断是否需要挂起当前线程
// 关键操作:
// 1:确认前置节点是否有效,无效的话,一直往前找到状态不是取消的节点。
// 2: 把前置节点状态置为 SIGNAL。
// 1、2 两步操作,有可能一次就成功,有可能需要外部循环多次才能成功,但最后一定是可以成功的
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;
// 如果前一个节点 waitStatus 状态已经是SIGNAL了,直接返回,不需要在自旋了
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.
*/
// 找到前一个状态不是取消的节点,把当前 node 挂在有效节点身上
// 因为节点状态是取消的话,是无效的,是不能作为 node 的前置节点的,所以必须找到 node 的有效节点才行
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
// 否则直接把节点状态置 为SIGNAL
} 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;
}
- 如果当前节点的前驱结点的
waitStatus
为SINGAL
,说明前驱结点也在等待拿锁,因此当前节点可以挂起,直接返回true waitStatus>0
,则状态只能是CANCEL
,将其从队列中删除
waitStatus
是其他状态,当前节点已加入,则前置节点应做好准备等待锁,因此通过CAS操作将前置节点的waitStatus
置为SINGAL
返回false
,进行下一轮判断- 如果该方法返回
true
,代表当前节点需要被挂起,执行真正的挂起操作
parkAndCheckInterrupt()
/**
* Convenience method to park and then check if interrupted
*
* @return {@code true} if interrupted
*/
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);
return Thread.interrupted();
}
- 该方法挂起线程且检查当前线程是否被中断
LockSupport.park()
通过native方法来调用操作系统的原语操作将当前线程挂起interrupt()
返回当前线程的中断标识符,并将其复位为false
如果当前线程所在的结点处于头结点的后面一个,则会不断尝试获取锁,直到获取成功,否则进行判断是否需要挂起
如果当前线程所在结点之前除了head
还有其他节点,且waitStatus
为SINGAL
,当前节点就需要挂起,保证head之后只有一个节点通过CAS获取锁,队列中其他线程都以被挂起或正在被挂起,避免自旋消耗CPU
释放锁
release
protected boolean tryRelease(int arg) {
throw new UnsupportedOperationException();
}
- tryRelease()是AQS开放给上层业务自由实现的抽象方法,没有Override该方法却调用时会抛出异常
//unlock的基础方法
public final boolean release(int arg) {
// tryRelease 交给实现类去实现,一般就是用当前同步器状态减去 arg,如果返回 true 说明释放锁了
if (tryRelease(arg)) {
Node h = head;
// 头节点不为空,并且不是同步节点,就去释放
if (h != null && h.waitStatus != 0)
//从头开始唤醒等待锁的节点
unparkSuccessor(h);
return true;
}
return false;
}
- release方法中,若尝试释放锁成功,则唤醒等待队列中其他节点
unparkSuccessor()
// 当线程释放锁成功后,从 node 开始唤醒同步队列中的节点
// 通过唤醒机制,保证线程不会一直在同步队列中阻塞等待
private void unparkSuccessor(Node node) {
int ws = node.waitStatus;
// node 节点是当前释放锁的节点
// 把节点的状态置为初始化
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
// 拿出 node 节点的后面一个节点
Node s = node.next;
// s 为空,代表队列中没有其他等待的节点了
// s.waitStatus 大于0,代表 s 节点已经被取消了
// 遇到以上这两种情况,就从队尾开始,向前遍历,找到第一个 waitStatus 不是取消的
if (s == null || s.waitStatus > 0) {
s = null;
// 这里从尾迭代,而不是从头开始迭代是有原因的。
// 主要是因为节点被阻塞的 acquireQueued 方法里面,节点在 acquireQueued 方法被阻塞,唤醒时也一定会在
// acquireQueued 方法里面被唤醒,唤醒之后的条件是,判断当前节点的前置节点是否是头节点,这里是判断当前节点的
// 前置节点,所以这里必须使用从尾到头的迭代顺序才行
for (Node t = tail; t != null && t != node; t = t.prev)
// t.waitStatus <= 0 说明 t 没有被取消,肯定还在等待被唤醒
if (t.waitStatus <= 0)
s = t;
}
// 唤醒以上代码找到的线程
if (s != null)
LockSupport.unpark(s.thread);
}
- node的状态为负值时,为防止Node被意外唤醒,需要用CAS操作将其置1
- 当node的后继结点不为null,则直接将其唤醒,为null时从尾到头遍历应该被唤醒的第一个结点
- 之前挂起又被唤醒的结点,继续执行
acquireQueued
方法,自旋尝试获取锁
interrupt
如果一个线程通过wait
,sleep
方法进行挂起,再调用interrupt
方法会直接抛出异常,而使用LockSupport.park()
挂起,即时调用interrupt
方法,也不会抛出异常,若其他线程调用该线程的interrupt
方法,只会改变该线程对象内部的一个中断的状态值,并使park返回
在AQS中,线程处于等待队列中时无法响应外部的中断请求,只有当线程拿到锁后,然后再进行中断响应