一 背景
之前在b站看了些AQS的视频。在看的过程中,总是在想:当遇到一些比较临界
的场景时。AQS时如何保证并发安全的,于是就对这些场景进行模拟。通过debug来搞清楚逻辑,
在实验前,有几点说明:
- 之后的代码时
基于jdk8公平锁
(我觉得两个锁的区别不是很大`) - 我的理解中,并发编程时宏观上并行,微观上串行(
补充:后来查阅到java在多核cpu中可以并行执行!!!再次对下面的并发场景进行分析,发现代码依旧安全.所以串行执行这个前提可以去掉!
) - AQS中对一些关键字段(
用来判断状态
)作了volatile修饰,保证了只要数据被修改。就对所有的线程可见 - AQS中的阻塞是通过
LockSupport
这个类来实现的,并且如果先unpark
某个线程,之后该线程park。改线程不会阻塞
- 当线程较多时,AQS会使用队列来存储一个个阻塞的线程。前一个线程会唤醒下一个(
第一个排队
)的线程 - 队列的第一个节点
不存储阻塞的线程
。其thread字段为null
。我的理解时该Node代表一个可能
正在执行的线程
二 如果前一个线程先unpark会怎么样
上面提过了,先unpark
,在park。线程不会阻塞
。不会出现无法唤醒的场景
三 如果前一个线程后unpark会怎么样
这应该是比较普遍的情况。这样前一个线程释放锁后,后一个线程就会被唤醒,然后往下执行(就是拿到锁了
)
四 就是后一个线程刚加入队列,但前一个线程已经释放锁
情况一
因为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)) {//for循环第一次会为true
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
if (shouldParkAfterFailedAcquire(p, node) &&//主要就是一步
parkAndCheckInterrupt())//这一步就是park
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
我们进入shouldParkAfterFailedAcquire
方法
第一次进来时pred的waitStatus肯定为0.因为前一个线程对应Node的status就是后面一个线程设置
的,该线程第一次进来。肯定还是默认值0.
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
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;//因为是false。还会走外面的for循环
}
注意该场景的前提是前一个线程此时已经unpark
了。那么该锁就是空的状态。而且字段都是volatile的,所以当前线程可见
再次判断。下面条件肯定成立,当前线程不阻塞。相当于获取到了锁,继续往下执行
if (p == head && tryAcquire(arg)) {
情况三 park前一步时间片用完,unpark结束或跳过
线程A先执行到这一步
另一个线程B开始继续执行
说实话,如果真出现了这种场景。那么确实第一个线程会一直阻塞。现在我们需要分析。这种场景到底会不会出现。
我们先分析线程B
执行到这一步(return ture前面
)之前的几种情况,
也就是这一步的情况
4.1 h=null
那说明B线程还没走到下面这一步(这次声明:微观串行+可见性!!!是可以得出下面这个结论的
)无法想到反例
那么之后B线程必然会在下面一步退出,那么线程A就不可能到park上面这一步。所以该情况不成立!
if (p == head && tryAcquire(arg))
4.2 h!=null &&waitStatus =0
此时场景就回到了情况二(不是情况二,但很像!情况二是更后面的状态,明确释放了锁.
)(注意:释放锁,并不代表一定要upark。我这里是指线程退出这个unlock方法),
waitStatus=0说明什么?
说明线程A还没有执行过下面这一步
但当B线程这一步时,B线程实际已经修改过state了
那么线程A必然会在后面通过if条件实现获取锁,也就不会走到park那一步了
if (p == head && tryAcquire(arg))
4.3 h!=null &&waitStatus !=0
此时线程B会执行Unpark(B线程)。那么无论如何,B线程都不可能锁死.
继续扩展一下
那有没有可能unpark了线程A,但A线程却没有park呢?
可以确定的说: 可以! 因为我在本地通过debug模拟了这种情况,可以发生。
五 总结
acquireQueued
方法里的for循环,多次判断有点精髓
。总感觉自己有点悟,但抓不到重点。
但还是记录一下
- trylock时为什么先修改state?如果将
tryRelease
方法放后面执行
,那么肯定会出现一直阻塞的场景
。可以想象一下情况4.1 - 如何保证线程阻塞了,那么
一定会在某个时刻被唤醒
。其实就是上面几种场景的分析。
核心思想
就三句话
- 规定线程park前,需要将前一个node的waitStatus设置为
其他值(非零)
- 如果明确有
后任节点(waitStatus!=0时,这个后任节点就是第一个排队的节点,位置是第二个)
,那就不管三七二十一,unpark这个节点的线程(不管它是不是用的到
) - 如果前一个线程没有执行unpark了(
已成事实
),为了保证不出现问题。只能在后任节点线程park前多做几次检查
。因为没有执行unpark(推断出当时waitStatus=0)
,并且肯定已经设置state=0了
,那么后任节点只要在修改waitStatus前(也就是park前
)检查state即可