SecondaryLoop.enter() 在 EDT 上调用 exit() 之前不会阻塞

SecondaryLoop.enter() not blocking until exit() is called on the EDT

总结

由于某些原因,当我调用 SecondaryLoop.enter() on the AWT Event Dispatch Thread (EDT), it does not wait for SecondaryLoop.exit() 时,在解除阻塞之前被调用。

背景

因为我觉得SecondaryLoop不是很出名的class,所以简单介绍一下:

一般来说,在 EDT 上使用任何长时间执行或阻塞的代码 运行 是个坏主意,因为那样的话,您的应用程序将不会响应任何事件,直到该代码终止。 EventQueue.createSecondaryLoop() 允许您创建一个新的事件循环来处理事件,允许您在不损失响应能力的情况下阻止 EDT。这就是 swing 模式对话框用来允许您在等待对话框关闭时阻止您的 EDT,但仍然允许对话框本身的控件能够运行。

创建 SecondaryLoop 实例后,您应该可以调用 enter(),它应该会阻塞,直到 exit() 被调用。

来自文档

This method can be called by any thread including the event dispatch thread. This thread will be blocked until the exit() method is called or the loop is terminated. A new secondary loop will be created on the event dispatch thread for dispatching events in either case.

虽然我不完全确定 "or the loop is terminated" 是什么意思。那可能是我的问题。

测试代码

在 EDT 以外的线程上调用 enter() 方法,如我所料,会阻塞:

System.out.println("Enter Loop");
Toolkit.getDefaultToolkit().getSystemEventQueue().createSecondaryLoop().enter();
System.out.println("Done (we should never get here)");

输出:

Enter Loop

但是,如果我们在美国东部时间调用它,它会阻塞大约一秒钟,然后继续:

System.out.println("Enter Loop");
try {
    SwingUtilities.invokeAndWait(() -> Toolkit.getDefaultToolkit().getSystemEventQueue().createSecondaryLoop().enter());
} catch (InvocationTargetException | InterruptedException e) {
    e.printStackTrace();
}
System.out.println("Done (we should never get here)");

输出:

Enter Loop
Done (we should never get here)

根据 tevemadar 的评论(感谢 BTW),我更新了代码以防止任何可能的垃圾收集问题:

//Storing loop in array as a quick hack to get past the
// "final or effectively final" issue when using this in the invokeAndWait
SecondaryLoop loop[] = new SecondaryLoop[1];

System.out.println("Enter Loop");
try {
    SwingUtilities.invokeAndWait(() -> {
        loop[0] = Toolkit.getDefaultToolkit().getSystemEventQueue().createSecondaryLoop();
        loop[0].enter();
    });
} catch (InvocationTargetException | InterruptedException e) {
    e.printStackTrace();
}
System.out.println("Done (we should never get here)");
//Just printing this to make sure that it is used after the invokeAndWait is done. This is just
//to make sure there isn't some sort of optimization thing that is deciding that we don't
//need this anymore and allowing the loop to be garbage collected
System.out.println(loop[0]);

输出:

Enter Loop
Done (we should never get here)
java.awt.WaitDispatchSupport@2401f4c3

所以,虽然这是一个很好的建议,但这似乎不是我的问题。

这似乎与文档相矛盾(对我来说 SecondaryLoop 的全部目的。我错过了什么吗?

环境

OS: Windows 10

Java:

C:\Program Files\Java\jre8\bin>java.exe -version
java version "1.8.0_221"
Java(TM) SE Runtime Environment (build 1.8.0_221-b11)
Java HotSpot(TM) 64-Bit Server VM (build 25.221-b11, mixed mode)

更新

凭直觉,我尝试添加一个计时器,不断向 EDT 循环添加更多事件。似乎添加计时器使循环保持活动状态并使其阻塞:

//Add a keep alive timer which adds an event to the EDT for every 0.5 sec
new Timer(500, null).start();

System.out.println("Enter Loop");
try {
    SwingUtilities.invokeAndWait(() -> Toolkit.getDefaultToolkit().getSystemEventQueue().createSecondaryLoop().enter());
} catch (InvocationTargetException | InterruptedException e) {
    e.printStackTrace();
}
System.out.println("Done (we should never get here)");

使用该代码,它会如我所料挂起,如果我在一段时间后放入一些调用循环中 exit() 方法的代码,它会如我所料终止。所以看起来循环必须在没有事件的情况下经过一定时间后自行终止(但前提是它最初是出于某种原因从 EDT 触发的......)。

我想我可以添加在需要使用此功能时什么都不做的计时器,但在我看来,这绝对更像是一种变通方法,而不是修复方法。

想通了(至少这个具体问题,我还有一些更相关的问题,但我希望我能自己解决)。

我决定在 java 源代码中开始调试,我意识到由于 java.awt.EventQueue:

中的这个段,我的线程被解锁了
    /**
     * Called from dispatchEvent() under a correct AccessControlContext
     */
    private void dispatchEventImpl(final AWTEvent event, final Object src) {
        event.isPosted = true;
        if (event instanceof ActiveEvent) {
            // This could become the sole method of dispatching in time.
            setCurrentEventAndMostRecentTimeImpl(event);
            ((ActiveEvent)event).dispatch();
        } else if (src instanceof Component) {
            ((Component)src).dispatchEvent(event);
            event.dispatched();
        } else if (src instanceof MenuComponent) {
            ((MenuComponent)src).dispatchEvent(event);
        } else if (src instanceof TrayIcon) {
            ((TrayIcon)src).dispatchEvent(event);
        } else if (src instanceof AWTAutoShutdown) {
            if (noEvents()) {
                dispatchThread.stopDispatching();
            }
        } else {
            if (getEventLog().isLoggable(PlatformLogger.Level.FINE)) {
                getEventLog().fine("Unable to dispatch event: " + event);
            }
        }
    }

在我的例子中 srcAWTAutoShutdown,这导致我的辅助循环在我调用 exit() 之前终止。

我发现 this article 这解释了为了确保事件队列最终在所有 windows 被处理后终止,awt 确定何时所有组件不再可显示并且事件队列为空,然后等待 1 秒,然后触发以 AWTAutoShutdown class 作为源的事件,终止事件队列并允许 JVM 终止。 1 秒超时是我观察到它会挂起一点的原因。

这解释了为什么添加计时器使我的代码工作(因为我每半秒添加一个事件并且 AWTAutoShutdown 的超时为 1 秒,事件队列将保持活动状态)。

所有这一切的用例基本上是创建一个 EDT 安全信号量,即使在 EDT 上等待事件(我用它来显示来自 Swing 应用程序的 JavaFX 对话框和使其表现得像一个原生的 swing 模态对话框)。所以在我的实际用例中,这应该工作得很好(因为在我的实际应用程序中应该总是显示一些 swing 组件)。但是,我什至没有真正尝试过我的实际用例。作为 TDD 的忠实信徒,我首先关注我的 JUnit 测试,它实际上并没有创建任何 UI 组件。

所以,我用一个确实有 GUI 的小虚拟应用程序做了一个快速测试,它工作得很好。我只是将我的 500 毫秒计时器添加到我的单元测试中,并在每次测试之前启动和停止它。

这样做之后,我仍然 运行 遇到一些测试问题,但我的原始问题中的最小可验证示例工作得很好。我将深入研究剩余的测试失败并希望自己解决。如果它似乎是一个相关的问题,那么我将添加一个新的 SO 问题并在此处添加 link。