|
**接下来通过案例分析,为什么需要 `PROPAGATE` 状态?** |
|
|
|
在共享模式下,线程获取和释放资源的方法调用链如下: |
|
|
|
- 线程获取资源的方法调用链为: `acquireShared() -> tryAcquireShared() -> 线程阻塞等待唤醒 -> tryAcquireShared() -> setHeadAndPropagate() -> if (剩余资源数 > 0) || (head.waitStatus < 0) 则唤醒后续节点` 。 |
|
|
|
- 线程释放资源的方法调用链为: `releaseShared() -> tryReleaseShared() -> doReleaseShared()` 。 |
|
|
|
**如果在释放资源时,没有将 `head` 节点的状态由 `0` 改为 `PROPAGATE` :** |
|
|
|
假设总共有 4 个线程尝试以共享模式获取资源,总共有 2 个资源。初始 `T3` 和 `T4` 线程获取到了资源,`T1` 和 `T2` 线程没有获取到,因此在队列中排队等候。 |
|
|
|
- 在时刻 1 时,线程 `T1` 和 `T2` 在等待队列中,`T3` 和 `T4` 持有资源。此时等待队列内节点以及对应状态为(括号内为节点的 `waitStatus` 状态): |
|
|
|
`head(-1) -> T1(-1) -> T2(0)` 。 |
|
|
|
- 在时刻 2 时,线程 `T3` 释放资源,通过 `doReleaseShared()` 方法将 `head` 节点的状态由 `SIGNAL` 更新为 `0` ,并唤醒线程 `T1` ,之后线程 `T3` 退出。 |
|
|
|
线程 `T1` 被唤醒之后,通过 `tryAcquireShared()` 获取到资源,但是此时还未来得及执行 `setHeadAndPropagate()` 将自己设置为 `head` 节点。此时等待队列内节点状态为: |
|
|
|
`head(0) -> T1(-1) -> T2(0)` 。 |
|
|
|
- 在时刻 3 时,线程 `T4` 释放资源, 由于此时 `head` 节点的状态为 `0` ,因此在 `doReleaseShared()` 方法中无法唤醒 `head` 的后继节点, 之后线程 `T4` 退出。 |
|
|
|
- 在时刻 4 时,线程 `T1` 继续执行 `setHeadAndPropagate()` 方法将自己设置为 `head` 节点。 |
|
|
|
但是此时由于线程 `T1` 执行 `tryAcquireShared()` 方法返回的剩余资源数为 `0` ,并且 `head` 节点的状态为 `0` ,因此线程 `T1` 并不会在 `setHeadAndPropagate()` 方法中唤醒后续节点。此时等待队列内节点状态为: |
|
|
|
`head(-1,线程 T1 节点) -> T2(0)` 。 |
|
|
|
此时,就导致线程 `T2` 节点在等待队列中,无法被唤醒。对应时刻表如下: |
|
|
|
| 时刻 | 线程 T1 | 线程 T2 | 线程 T3 | 线程 T4 | 等待队列 | |
|
| ------ | -------------------------------------------------------------- | -------- | ---------------- | ------------------------------------------------------------- | --------------------------------- | |
|
| 时刻 1 | 等待队列 | 等待队列 | 持有资源 | 持有资源 | `head(-1) -> T1(-1) -> T2(0)` | |
|
| 时刻 2 | (执行)被唤醒后,获取资源,但未来得及将自己设置为 `head` 节点 | 等待队列 | (执行)释放资源 | 持有资源 | `head(0) -> T1(-1) -> T2(0)` | |
|
| 时刻 3 | | 等待队列 | 已退出 | (执行)释放资源。但 `head` 节点状态为 `0` ,无法唤醒后继节点 | `head(0) -> T1(-1) -> T2(0)` | |
|
| 时刻 4 | (执行)将自己设置为 `head` 节点 | 等待队列 | 已退出 | 已退出 | `head(-1,线程 T1 节点) -> T2(0)` | |
|
|
|
**如果在线程释放资源时,将 `head` 节点的状态由 `0` 改为 `PROPAGATE` ,则可以解决上边出现的并发问题,如下:** |
|
|
|
- 在时刻 1 时,线程 `T1` 和 `T2` 在等待队列中,`T3` 和 `T4` 持有资源。此时等待队列内节点以及对应状态为: |
|
|
|
`head(-1) -> T1(-1) -> T2(0)` 。 |
|
|
|
- 在时刻 2 时,线程 `T3` 释放资源,通过 `doReleaseShared()` 方法将 `head` 节点的状态由 `SIGNAL` 更新为 `0` ,并唤醒线程 `T1` ,之后线程 `T3` 退出。 |
|
|
|
线程 `T1` 被唤醒之后,通过 `tryAcquireShared()` 获取到资源,但是此时还未来得及执行 `setHeadAndPropagate()` 将自己设置为 `head` 节点。此时等待队列内节点状态为: |
|
|
|
`head(0) -> T1(-1) -> T2(0)` 。 |
|
|
|
- 在时刻 3 时,线程 `T4` 释放资源, 由于此时 `head` 节点的状态为 `0` ,因此在 `doReleaseShared()` 方法中会将 `head` 节点的状态由 `0` 更新为 `PROPAGATE` , 之后线程 `T4` 退出。此时等待队列内节点状态为: |
|
|
|
`head(PROPAGATE) -> T1(-1) -> T2(0)` 。 |
|
|
|
- 在时刻 4 时,线程 `T1` 继续执行 `setHeadAndPropagate()` 方法将自己设置为 `head` 节点。此时等待队列内节点状态为: |
|
|
|
`head(-1,线程 T1 节点) -> T2(0)` 。 |
|
|
|
- 在时刻 5 时,虽然此时由于线程 `T1` 执行 `tryAcquireShared()` 方法返回的剩余资源数为 `0` ,但是 `head` 节点状态为 `PROPAGATE < 0` (这里的 `head` 节点是老的 `head` 节点,而不是刚成为 `head` 节点的线程 `T1` 节点)。 |
|
|
|
因此线程 `T1` 会在 `setHeadAndPropagate()` 方法中唤醒后续 `T2` 节点,并将 `head` 节点的状态由 `SIGNAL` 更新为 `0`。此时等待队列内节点状态为: |
|
|
|
`head(0,线程 T1 节点) -> T2(0)` 。 |
|
|
|
- 在时刻 6 时,线程 `T2` 被唤醒后,获取到资源,并将自己设置为 `head` 节点。此时等待队列内节点状态为: |
|
|
|
`head(0,线程 T2 节点)` 。 |
|
|
|
有了 `PROPAGATE` 状态,就可以避免线程 `T2` 无法被唤醒的情况。对应时刻表如下: |
|
|
|
| 时刻 | 线程 T1 | 线程 T2 | 线程 T3 | 线程 T4 | 等待队列 | |
|
| ------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------ | ---------------- | ------------------------------------------------------------------- | ------------------------------------ | |
|
| 时刻 1 | 等待队列 | 等待队列 | 持有资源 | 持有资源 | `head(-1) -> T1(-1) -> T2(0)` | |
|
| 时刻 2 | (执行)被唤醒后,获取资源,但未来得及将自己设置为 `head` 节点 | 等待队列 | (执行)释放资源 | 持有资源 | `head(0) -> T1(-1) -> T2(0)` | |
|
| 时刻 3 | 未继续向下执行 | 等待队列 | 已退出 | (执行)释放资源。此时会将 `head` 节点状态由 `0` 更新为 `PROPAGATE` | `head(PROPAGATE) -> T1(-1) -> T2(0)` | |
|
| 时刻 4 | (执行)将自己设置为 `head` 节点 | 等待队列 | 已退出 | 已退出 | `head(-1,线程 T1 节点) -> T2(0)` | |
|
| 时刻 5 | (执行)由于 `head` 节点状态为 `PROPAGATE < 0` ,因此会在 `setHeadAndPropagate()` 方法中唤醒后续节点,此时将新的 `head` 节点的状态由 `SIGNAL` 更新为 `0` ,并唤醒线程 `T2` | 等待队列 | 已退出 | 已退出 | `head(0,线程 T1 节点) -> T2(0)` | |
|
| 时刻 6 | 已退出 | (执行)线程 `T2` 被唤醒后,获取到资源,并将自己设置为 `head` 节点 | 已退出 | 已退出 | `head(0,线程 T2 节点)` | |
问题位置
文档路径:
docs/java/concurrent/aqs.md相关位置:
setHeadAndPropagate()源码及条件解释:JavaGuide/docs/java/concurrent/aqs.md
Lines 825 to 861 in 2dcbd22
doReleaseShared()与PROPAGATE说明:JavaGuide/docs/java/concurrent/aqs.md
Lines 866 to 900 in 2dcbd22
JavaGuide/docs/java/concurrent/aqs.md
Lines 902 to 980 in 2dcbd22
疑似问题
文档在解释 AQS 共享模式下
PROPAGATE的作用时,给出了一个示例:共享资源数为 2,T3、T4已经获取资源,T1、T2在 AQS 队列中等待。其中有一步推演大致为:
head(-1) -> T1(-1) -> T2(0);T3释放资源,将head.waitStatus从SIGNAL(-1)改为0,并唤醒T1;T1被唤醒后成功tryAcquireShared(),但还没有执行setHeadAndPropagate();T4此时释放资源,由于当前head.waitStatus == 0,所以doReleaseShared()无法唤醒后继;T1执行setHeadAndPropagate(),文档推演认为由于propagate == 0且head.waitStatus == 0,所以不会继续唤醒T2,导致T2无法被唤醒。这里我理解可能有一点遗漏:
setHeadAndPropagate()并不是只判断propagate > 0和旧head.waitStatus < 0,它还会在setHead(node)之后重新读取新的head并再次判断waitStatus < 0。源码依据
以 OpenJDK 8u 中的
AbstractQueuedSynchronizer#setHeadAndPropagate()为例,关键判断如下:源码位置:https://github.com/openjdk/jdk8u/blob/master/jdk/src/share/classes/java/util/concurrent/locks/AbstractQueuedSynchronizer.java#L708-L731
这里包含两次
head判断:h.waitStatus < 0判断的是setHead(node)之前保存的旧head;(h = head) == null || h.waitStatus < 0判断的是setHead(node)之后重新读取到的新head。具体推演
如果按照文档示例,在
T1执行setHeadAndPropagate()前队列是:那么
T1执行setHeadAndPropagate(T1, propagate)时:head,此时旧head.waitStatus == 0;setHead(T1),T1成为新的head;head就是原来的T1节点;T1节点的waitStatus是-1;(h = head).waitStatus < 0时条件成立;if;Node s = node.next,也就是T1.next;T2是共享节点,则会调用doReleaseShared();doReleaseShared()会继续尝试唤醒T2。也就是说,在这个状态下:
T1后续执行setHeadAndPropagate()时,似乎不会因为propagate == 0且旧head.waitStatus == 0就停止传播。因为方法还会检查更新后的新head,也就是原来的T1节点;如果T1.waitStatus == -1,那么新head.waitStatus < 0条件仍然成立。因此,文档中“由于
propagate == 0且head.waitStatus == 0,所以T1不会在setHeadAndPropagate()中唤醒后继节点,导致T2无法唤醒”的推演,疑似遗漏了setHead(node)之后对新head.waitStatus的再次判断。以上是我阅读源码后的理解,也可能是我对该并发场景的理解存在偏差。如果有理解不准确的地方,也欢迎指正。
感谢维护者整理 JavaGuide。