【问题标题】:SecondaryLoop.enter() not blocking until exit() is called on the EDT在 EDT 上调用 exit() 之前,SecondaryLoop.enter() 不会阻塞
【发布时间】:2020-01-31 04:07:29
【问题描述】:

总结

由于某种原因,当我在 AWT 事件调度线程 (EDT) 上调用 SecondaryLoop.enter() 时,它不会在解除阻塞之前等待调用 SecondaryLoop.exit()

背景

由于我认为SecondaryLoop不是一个非常知名的类,所以我将简要概述一下:

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

创建SecondaryLoop 实例后,您应该能够调用enter(),并且它应该阻塞直到调用exit()

来自文档

这个方法可以被任何线程调用,包括事件调度线程。在调用 exit() 方法或终止循环之前,该线程将被阻塞。无论哪种情况,都会在事件调度线程上创建一个新的辅助循环来调度事件。

不过,我不完全确定“或循环终止”是什么意思。那可能是我的问题。

测试代码

在除 EDT 之外的线程上调用 enter() 方法,正如我所期望的那样阻塞:

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

输出:

Enter Loop

但是,如果我们在 EDT 上调用它,它会阻塞大约一秒钟,然后继续:

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 的评论(感谢顺便说一句),我已更新代码以防止任何可能的垃圾收集问题:

//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 对我的全部目的)相矛盾。我错过了什么吗?

环境

操作系统: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 触发的......)。

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

【问题讨论】:

  • 我认为你用错了:SecondaryLoop 应该被保留,所以你可以稍后调用它的exit()。因为这里没有保留,可能是 GC 介入了,实际实现中可能有一个终结器退出循环。
  • 感谢 tevemadar。这是一个很好的建议,但是我遇到的实际代码确实存储了循环。这只是我最小的可重现示例(没有存储循环)。我创建了一个新的最小可重现示例,它确实存储了循环并更新了问题。不幸的是,问题仍然存在。

标签: java awt event-dispatch-thread


【解决方案1】:

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

我决定在 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,它解释了为了确保事件队列最终在所有窗口都被释放时终止,awt 确定所有组件何时不再可显示并且事件队列为空,然后等待 1 秒,然后以AWTAutoShutdown 类作为源触发事件,终止事件队列并允许JVM 终止。 1 秒的超时是我观察到它会挂起一点的原因。

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

所有这一切的用例基本上是创建一个 EDT 安全的信号量,即使在 EDT 上等待事件(我用它来显示来自 Swing 应用程序的 JavaFX 对话框并制作它),它也允许事件继续执行表现得像一个原生的摇摆模式对话框)。所以在我的实际用例中,这应该可以正常工作(因为在我的实际应用程序中应该总是显示一些摆动组件)。但是,我什至没有真正尝试过我的实际用例。作为 TDD 的忠实信徒,我首先专注于我的 JUnit 测试,它实际上并没有创建任何 UI 组件。

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

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

【讨论】:

猜你喜欢
  • 2015-07-19
  • 1970-01-01
  • 2023-01-18
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2021-10-19
  • 1970-01-01
相关资源
最近更新 更多