PAYATHON 2026

Java 如何检测等待线程是否收到通知,并观察通知选择顺序?

数据阿明

能否直接查询线程是否已收到通知?

Java 标准 API 没有提供方法,直接查询某个线程是否已被 Object.notify() 选中。notify() 的返回类型是 void,不会返回被选中的线程。

notify() 不保证按 FIFO、round robin 或随机方式选择线程。规范允许实现自行决定选择哪个等待线程;“顺序未规定”也不等于“均匀随机”。即使在某个环境中观察到稳定规律,也不能以此作为程序正确性的依据。Object.notify() 文档

为什么 getState() 不够?

getState() 返回的是线程状态的瞬时快照,不包含导致状态变化的事件历史。它适合用于监控,不能作为线程之间的同步协议。Thread.getState() 文档

需要区分三个时刻:

  1. 线程被通知,从对象的 wait set 中移除。
  2. 线程重新获得该对象的 monitor。
  3. 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() 的差值计算。

为了减少实验中的混杂因素,可以按以下方式组织测试:

  1. 让所有参与线程使用同一个 monitor,通过受同步保护的就绪协议协调等待阶段,避免用固定 sleep() 猜测线程是否已经就绪。
  2. 每轮只调用一次 notify(),观察后续恢复事件后再开始下一轮,减少多次通知重叠。
  3. 单独记录中断、超时和未按预期完成的轮次,不强行将每次返回与一次通知配对。
  4. 改变线程启动顺序并重复运行,同时记录 JDK、JVM 和操作系统信息,限定结论的适用范围。

这些方法记录的是应用层可见的等待与恢复行为。尤其在多次通知重叠时,恢复顺序还受到重新竞争 monitor 和线程调度的影响,不能直接等同于 JVM 选择通知对象的顺序。

示例中的同步输出会干扰调度。做统计实验时,可以将事件写入预分配缓冲区,测试结束后统一导出,但记录操作本身仍有开销。

如果要研究某个 JVM 的精确选择算法,需要进一步检查对应版本的实现或增加运行时级别的探针;普通应用层日志无法直接暴露内部选择事件。如果业务必须保证 FIFO 或轮询顺序,应显式实现排队与交接协议,避免依赖实验中观察到的 notify() 顺序。

备注:内容仅供参考。