事件调度线程是如何工作的?

Posted

技术标签:

【中文标题】事件调度线程是如何工作的?【英文标题】:How does the event dispatch thread work? 【发布时间】:2011-01-29 21:02:55 【问题描述】:

在*** 上的人们的帮助下,我能够获得以下简单 GUI 倒计时的工作代码(它只显示一个倒计时的窗口)。这段代码的主要问题是invokeLater 的东西。

据我了解invokeLater,它会向事件调度线程 (EDT) 发送一个任务,然后 EDT 在“可以”时执行此任务(无论这意味着什么)。 对吗?

据我了解,代码是这样工作的:

    main 方法中,我们使用invokeLater 来显示窗口(@98​​7654327@ 方法)。换句话说,显示窗口的代码将在 EDT 中执行。

    main 方法中,我们还启动counter 并且计数器(通过构造)在另一个线程中执行(因此它不在事件调度线程中)。对吧?

    counter 在单独的线程中执行,并定期调用updateGUIupdateGUI 应该更新 GUI。 GUI 在 EDT 中工作。所以,updateGUI 也应该在 EDT 中执行。这就是updateGUI 的代码包含在invokeLater 中的原因。对吗?

我不清楚为什么我们从 EDT 调用 counter。无论如何,它不会在 EDT 中执行。它立即启动,一个新线程和counter 在那里执行。那么,为什么我们不能在 invokeLater 块之后的 main 方法中调用 counter

import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.SwingUtilities;

public class CountdownNew 

    static JLabel label;

    // Method which defines the appearance of the window.   
    public static void showGUI() 
        JFrame frame = new JFrame("Simple Countdown");
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        label = new JLabel("Some Text");
        frame.add(label);
        frame.pack();
        frame.setVisible(true);
    

    // Define a new thread in which the countdown is counting down.
    public static Thread counter = new Thread() 
        public void run() 
            for (int i=10; i>0; i=i-1) 
                updateGUI(i,label);
                try Thread.sleep(1000); catch(InterruptedException e) ;
            
        
    ;

    // A method which updates GUI (sets a new value of JLabel).
    private static void updateGUI(final int i, final JLabel label) 
        SwingUtilities.invokeLater( 
            new Runnable() 
                public void run() 
                    label.setText("You have " + i + " seconds.");
                
            
        );
    

    public static void main(String[] args) 
        SwingUtilities.invokeLater(new Runnable() 
            public void run() 
                showGUI();
                counter.start();
            
        );
    


【问题讨论】:

@Roman 这里有更详细的讨论:***.com/questions/182316/… @Roman 注意: 您的计数器在 EDT 上启动,它在主线程上启动。计数器通过 updateGUI 方法更新 GUI,该方法在 EDT 上进行更新(因为调用了 invokeLater)。 【参考方案1】:

您实际上是开始来自 EDT 的 counter 线程。如果您在invokeLater 块之后调用counter.start(),则计数器可能会开始运行 GUI 变得可见之前。现在,因为您在 EDT 中构建 GUI,所以当 counter 开始更新 GUI 时,GUI 不会存在。幸运的是,您似乎正在将 GUI 更新转发到 EDT,这是正确的,并且由于 EventQueue 是一个队列,第一次更新将在 GUI 构建后发生,因此应该没有理由不这样做。但是更新可能还不可见的 GUI 有什么意义呢?

【讨论】:

谢谢。你说的对我来说很有道理。那么,如果我在 invokeLater 延迟一段时间后启动计数器,它应该可以工作吗? 实际上我在第一个版本的答案中犯了一个错误。我认为它应该双向工作。 @Roman 不,它仍然行不通......时间延迟不会给你一个保证,它只是为你争取时间,你仍然有比赛条件.你不想掩盖你的竞争条件,你想消除它们。 Joonas Pulakka,但为什么它应该双向工作?你在哪里做错了? @Joonas 他在主线程中启动计数器,而不是在 EDT 中。 EDT 和主线程是两个独立的东西。【参考方案2】:

如果我正确理解你的问题,你会奇怪为什么不能这样做:

public static void main(String[] args) 
    SwingUtilities.invokeLater(new Runnable() 
        public void run() 
            showGUI();
        
    );
    counter.start();

你不能这样做的原因是因为调度程序没有保证......仅仅因为你调用showGUI()然后你调用counter.start()并不意味着showGUI()中的代码将被执行在counter的run方法中的代码之前。

这样想:

invokeLater 启动一个线程,该线程在 EDT 上调度一个异步事件,该事件的任务是创建JLabel。 计数器是一个单独的线程,它依赖于JLabel 的存在,因此它可以调用label.setText("You have " + i + " seconds.");

现在你有一个竞争条件: JLabel 必须在 counter 线程开始之前创建,如果它不是在计数器线程开始之前创建的,那么你的计数器线程将调用 @987654334 @ 在未初始化的对象上。

为了确保消除竞争条件,我们必须保证执行顺序和一种方式保证它是在同一个线程上顺序执行showGUI()counter.start()

public static void main(String[] args) 
    SwingUtilities.invokeLater(new Runnable() 
        public void run() 
            showGUI();
            counter.start();
        
    );

现在showGUI();counter.start(); 从同一个线程执行,因此JLabel 将在counter 启动之前创建。

更新:

问: 我不明白这个线程有什么特别之处。答: Swing 事件处理代码运行在称为事件调度线程的特殊线程。大多数调用 Swing 方法的代码也在这个线程上运行。这是必要的,因为大多数 Swing 对象方法都不是“线程安全的”:从多个线程调用它们可能会导致线程干扰或内存一致性错误。 1

问: 那么,如果我们有一个 GUI,为什么要在单独的线程中启动它?答:可能比我的答案更好,但如果你想从 EDT 更新 GUI(你这样做),那么你必须从 EDT 启动它。

问: 为什么我们不能像其他任何线程一样启动线程?答:请参阅上一个答案。

问: 为什么我们使用一些 invokeLater 以及为什么这个线程 (EDT) 在准备好时开始执行请求。为什么它并不总是准备好?答: EDT 可能还有一些其他 AWT 事件需要处理。 invokeLater 导致 doRun.run() 在 AWT 事件分派线程上异步执行。这将在处理完所有待处理的 AWT 事件后发生。当应用程序线程需要更新 GUI 时,应使用此方法。 2

【讨论】:

invokeLater 不会启动新线程。相反,它安排 Runnable 在现有的 AWT 事件调度线程中运行。 @Steve 谢谢,我更正了这条线。如果您注意到在 Q/AI 的最后一段中复制了文档,并且它特别指出:“[invokeLater 导致] doRun.run() 在 AWT 事件调度线程上异步执行。” 我的无论哪种方式,对比赛条件的评估都是正确的。 这里没有理由不能使用invokeAndWait,对吧?这将使当前线程等待直到可运行对象执行完毕。 @Chris 我认为 OP 在识别竞争条件时遇到了麻烦,但是对于这个问题有 很多 解决方案。 我对此越了解越困惑。我同意调度程序不保证任务何时运行,但根据 (java.sun.com/products/jfc/tsc/articles/threads/threads1.html) “事件以可预测的顺序调度”的最后一部分,这让我认为我们不知道何时,但 showGUI 总是在 updateGUI 之前排队,并且以该顺序为准。如果是这样,在 EDT 之外启动计数器线程应该不是问题......你们怎么看?【参考方案3】:

什么是 EDT?

这是一个解决 Swing API 存在的大量并发问题的 hacky 解决方法;)

说真的,很多 Swing 组件都不是“线程安全的”(一些著名的程序员甚至称 Swing 为“线程敌对”)。通过拥有一个独特的线程,所有更新都在这个线程敌对组件中进行,您可以避免很多潜在的并发问题。除此之外,您还可以保证它会按顺序运行您使用invokeLater 通过它的Runnable

然后是一些吹毛求疵:

public static void main(String[] args) 
    SwingUtilities.invokeLater(new Runnable() 
        public void run() 
            showGUI();
            counter.start();
        
    );

然后:

在 main 方法中,我们还启动 计数器和计数器(由 施工)在另一个执行 线程(所以它不在事件中 调度线程)。对吧?

您并没有真正在 main 方法中启动计数器。您在 EDT 上执行的匿名 Runnablerun() 方法中启动计数器。所以你真的开始从 EDT 的计数器 Thread,而不是主要方法。然后,因为它是一个单独的线程,它不在 EDT 上运行。但是计数器肯定在 EDT 上启动的,而不是在 Thread 执行 main(...) 方法中。

这很吹毛求疵,但看到我认为的问题仍然很重要。

【讨论】:

WizardOfOdds,我了解该计数器未在 EDT 上运行。它在一个单独的线程中运行,该线程(线程)从 EDT 启动。我也知道计数器没有在与 main 方法相同的线程中运行。我只是想说main方法将showGUIcounter.start发送到EDT,counter.start从EDT启动一个新线程。 @Roman: 是的,完全正确...但重要的是要正确措辞,以防其他人阅读此问题/答案:)【参考方案4】:

这个很简单,如下

第 1 步。创建初始线程,也称为主线程。

步骤 2. 创建一个可运行对象并将其传递给 invokeLate()。

第 3 步。这会初始化 GUI,但不会创建 GUI。

第 4 步。InvokeLater() 安排创建的对象在 EDT 上执行。

第 5 步。GUI 已创建。

第 6 步。所有发生的事件都将放在 EDT 中。

【讨论】:

以上是关于事件调度线程是如何工作的?的主要内容,如果未能解决你的问题,请参考以下文章

将逻辑线程与事件调度线程分离

swing中的线程

Windows XP线程的调度策略

事件同步-——CreateEvent( )事件对象实现线程同步

Gradle 错误:仅在 Android Studio 中允许从事件调度线程进行写访问

MySQL数据库高级——事件