回忆
现代诗之我和AQS
AQS这个玩意,
每次看,都看不进去。
这次耐着性子终于看完,
记录一篇博客,便于后面忘了快速回忆起来。
开始吧,
用我的方式。
背景
背景交代:
-
资源: 神医谷胡青牛诊室
-
队列:看病排队
-
节点: 一个个看病的人(线程 + 状态信息)
开篇花絮:
-
这时候,武当俞岱岩来治腿伤,看了一下有人排队, 想到武当怎么说也是名门正派,我也排一下吧,这就是公平锁。
-
不一会,金毛狮王谢逊来了,威风凛凛地看了一眼诊室,恰好韦一笑刚走出诊室,狮王大大咧咧直接进了诊室,队伍里一阵义愤填膺,窃窃私语,明教法王就可以不排队么,这太不公平了。这就是非公平锁。
接下来,我以ReentrantLock的非公平锁为例,结合武侠背景,走读一下加锁和释放锁的流程。
常遇春被截心掌所伤,前来神医谷看病。正在往诊室走来~
获取锁
获取锁 ReentrantLock NonfairSync lock() 这里是ReentrantLock自己实现的前奏。
代码段1
final void lock() {
if (compareAndSetState(0, 1))
// 常遇春看一眼诊室门开着且没人, 于是
// 直接进入诊室,并在门上贴上小纸条,常遇春在此,闲人勿进
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1); // MD,有人,于是陷入AQS内核
}
陷入AQS内核
代码段2
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
这里分三步:
-
tryAcquire 再看一眼,是否能抢进门,没人就冲进去,有人就算了
-
addWaiter 的确还有人,老实排到队伍的尾部(这时候,一小簇受伤的明教小兵过来了,也发现要排队,并正往队尾一路小跑,于是常遇春加快了而脚步)
-
acquireQueued 发呆,等着被前面的人叫醒或者冷醒(calling线程要在一个安全点等待)
第一步,tryAcquire (这个是AQS的用户自行实现对资源的获取)
代码段3
// NonfairSync类
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
// --
// NonfairSync 的父类 Sync
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
// 当前诊室没人,无需排队
if (c == 0) {
// cas, 防止蜂拥进去
if (compareAndSetState(0, acquires)) {
// 留下小纸条贴门上,来陪同的家人可以进来(可重入)
setExclusiveOwnerThread(current);
return true;
}
}
// 可重入
else if (current == getExclusiveOwnerThread()) {
// 重入后也要改变资源状态
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
// 没能冲进去,排队去吧
return false;
}
回到代码段2,如果tryAcquire失败,则开始排队,addWaiter 老实排队,过分地话,神医胡青牛会发飙。于是一群没抢进门的人蜂拥开始排队。
代码段4
// AQS类中
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;
// cas 冲刺排队
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
// 第一次冲刺排队失败,疯狂重试
enq(node);
return node;
}
代码段6
第一次冲刺排队失败后的疯狂重试
// AQS类中
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;
}
}
}
}
常遇春终于排好了队。接下来,干点啥打发一下时间呢,那时候还没有手机。睡着的话,万一轮到自己,后面这帮人肯定不会叫自己的。这时截心掌伤发作,哎呀~~~ 赶紧继续回到代码段2,acquireQueued,
代码段7
// AQS类中
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) { // 死循环,一定要排,誓不罢休
// 看一下前面的兄弟,是不是到他了
// 如果到他了,就开始跃跃欲试
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
// 常遇春:MD,终于轮到劳资了,一顿操作
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
代码段7中有2个要点,可以这样先这样去理解,在分析就容易了:
- shouldParkAfterFailedAcquire : 常遇春看了一下前面的兄弟,发现已经毒发身亡,摇了摇头,站到了这个人前面,并告诉前面的兄弟:“好兄弟,一会你出来的时候叫我一下,我伤有点重,想休息一下”
- parkAndCheckInterrupt:常遇春盘膝坐下,开始调养内息~ 循环过程中,如果排到自己,就返回到代码段2, 获取了资源,进入诊室。
代码段8
// AQS类中
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
// pred是前面的兄弟,看看他的状态
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;
}
代码段9
// AQS 类中
private final boolean parkAndCheckInterrupt() {
// 常遇春开始疗伤,头顶正在升起一股蒸汽~
LockSupport.park(this);
// 常遇春:MD,疗伤被打断了,看看到我了没,回到代码段7
// 注意返回值,线程是否被中断过~,后面补中断
return Thread.interrupted();
}
这里为啥要补中断呢,看一张图
线程park后,可能会被唤醒, 但是现在已经陷入AQS中(在这个无限循环里,不方便跳出来),醒来也是在循环里,所以记录一下中途被人叫醒过。
至此,常遇春已经拿到号,进入了诊室。
胡青牛以张无忌非明教中人,不予治疗,常遇春舍命替换,都是后话。 这里剧情稍微篡改一下。
好了,到此,锁的获取已经结束,下面开始说锁的释放。
释放锁
常遇春拿着药,骂骂咧咧走出来道:下一位,下一位!
后来常遇春被张无忌以猛药治好,但是损失了几十年阳寿,也是后话了~
时间稍微往回拉,我们看看刚才,看看常遇春是怎么被人叫醒的~
代码段10
// ReentrantLock
public void unlock() {
sync.release(1);
}
// AQS类
public final boolean release(int arg) {
// 韩千叶走出诊室,一脸惆怅,他开始释放诊室资源
if (tryRelease(arg)) {
// 资源有空闲(诊室空出来了),可以叫后面的兄弟了
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h); // 叫醒后面的兄弟
return true;
}
return false;
}
// ReentrantLock Sync类
protected final boolean tryRelease(int releases) {
// 释放资源
int c = getState() - releases;
// 非当前线程,无资格释放
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
// free的意思,是资源有空闲,可以叫后面的兄弟进来了
// 在ReentrantLock这种排它锁里,只有state为0,才表示有空闲
boolean free = false;
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
// 获取资源的就当前一个,所以是线程安全的,不需要cas
setState(c);
return free;
}
那如何叫醒后面的兄弟呢, 在unparkSuccessor里。
代码段11
// AQS
private void unparkSuccessor(Node node) {
/*
* If status is negative (i.e., possibly needing signal) try
* to clear in anticipation of signalling. It is OK if this
* fails or if status is changed by waiting thread.
* 这里,嗯,感觉没啥用~
*/
int ws = node.waitStatus;
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
/*
* Thread to unpark is held in successor, which is normally
* just the next node. But if cancelled or apparently null,
* traverse backwards from tail to find the actual
* non-cancelled successor.
* 看一眼后面的兄弟
*/
Node s = node.next;
if (s == null || s.waitStatus > 0) {
// 已经毒发了,或者被人叫走了
s = null;
// 那就从后往前找一遍,看看新来的,一直找到距离head最近的那个人
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t;
}
// 终于找到了下一个可以进诊室的人了, 你进去吧,喂喂喂
if (s != null)
LockSupport.unpark(s.thread);
}
接下来就可以回到 代码段9 以及 代码段7, 就看到常遇春怎么醒来的。