Java 如何检测等待线程是否收到通知,并观察通知选择顺序?
能否直接查询线程是否已收到通知?
Java 标准 API 没有提供方法,直接查询某个线程是否已被 Object.notify() 选中。notify() 的返回类型是 void,不会返回被选中的线程。
notify() 不保证按 FIFO、round robin 或随机方式选择线程。规范允许实现自行决定选择哪个等待线程;“顺序未规定”也不等于“均匀随机”。即使在某个环境中观察到稳定规律,也不能以此作为程序正确性的依据。Object.notify() 文档
为什么 getState() 不够?
getState() 返回的是线程状态的瞬时快照,不包含导致状态变化的事件历史。它适合用于监控,不能作为线程之间的同步协议。Thread.getState() 文档
需要区分三个时刻:
- 线程被通知,从对象的 wait set 中移除。
- 线程重新获得该对象的 monitor。
wait()返回,线程执行后续代码。
线程被通知后,仍需重新获得 monitor;通知者调用 notify() 时不会释放锁。因此,被选中的线程可能暂时处于 BLOCKED 状态,但看到线程处于 BLOCKED,也不能反推它刚刚收到了通知。Java 语言规范:等待与通知
wait() 正常返回也不一定意味着收到了通知:带超时的等待可能到期,无超时的等待也允许发生虚假唤醒。中断通常表现为 InterruptedException;如果中断与通知并发发生,还需要考虑两者的先后关系。Object.wait() 文档
如何记录等待与恢复过程?
如果可以修改测试程序,可以在等待端和通知端记录事件。下面的组件以许可计数作为等待条件,记录进入等待、正常返回、中断和消耗许可的过程:
public final class WaitProbe {
private final Object monitor = new Object();
private int permits;
public void awaitPermit() throws InterruptedException {
synchronized (monitor) {
while (permits == 0) {
log("WAIT_BEGIN");
try {
monitor.wait();
log("WAIT_RETURN");
} catch (InterruptedException e) {
log("INTERRUPTED");
throw e;
}
}
permits--;
log("PERMIT_ACQUIRED");
}
}
public void signalOne() {
synchronized (monitor) {
permits++;
log("NOTIFY_CALL");
monitor.notify();
}
}
private static void log(String event) {
System.out.printf("%d,%s,%s%n",
System.nanoTime(),
Thread.currentThread().getName(),
event);
}
}
让多个工作线程共享同一个 WaitProbe 实例并调用 awaitPermit(),由控制线程调用 signalOne()。工作线程应在任务边界处理 InterruptedException,例如恢复中断标志后退出。
代码使用 while 检查条件,以处理虚假唤醒,以及线程恢复执行时条件已不满足的情况。等待、条件检查和条件更新都使用同一个 monitor。Java 语言规范:wait
各事件的含义如下:
WAIT_BEGIN:线程即将调用wait()。NOTIFY_CALL:通知者即将调用notify(),不表示已识别接收者。WAIT_RETURN:线程已重新获得 monitor,且wait()正常返回。PERMIT_ACQUIRED:线程成功消耗了一个应用层许可。
许可会保存在计数中,通知本身则不会留给未来的等待者。因此,许可消费次数不能直接当作通知接收次数。如果提前积累了许可,后续线程甚至可能无需等待就继续执行。
如何统计是否存在选择规律?
可以将事件导出为 CSV,按线程绘制时间线,统计等待次序、返回次序、各线程恢复次数和等待时长。同一 JVM 内的时间间隔可用 System.nanoTime() 的差值计算。
为了减少实验中的混杂因素,可以按以下方式组织测试:
- 让所有参与线程使用同一个 monitor,通过受同步保护的就绪协议协调等待阶段,避免用固定
sleep()猜测线程是否已经就绪。 - 每轮只调用一次
notify(),观察后续恢复事件后再开始下一轮,减少多次通知重叠。 - 单独记录中断、超时和未按预期完成的轮次,不强行将每次返回与一次通知配对。
- 改变线程启动顺序并重复运行,同时记录 JDK、JVM 和操作系统信息,限定结论的适用范围。
这些方法记录的是应用层可见的等待与恢复行为。尤其在多次通知重叠时,恢复顺序还受到重新竞争 monitor 和线程调度的影响,不能直接等同于 JVM 选择通知对象的顺序。
示例中的同步输出会干扰调度。做统计实验时,可以将事件写入预分配缓冲区,测试结束后统一导出,但记录操作本身仍有开销。
如果要研究某个 JVM 的精确选择算法,需要进一步检查对应版本的实现或增加运行时级别的探针;普通应用层日志无法直接暴露内部选择事件。如果业务必须保证 FIFO 或轮询顺序,应显式实现排队与交接协议,避免依赖实验中观察到的 notify() 顺序。
备注:内容仅供参考。
