事件分派线程是如何工作的?

12
stackoverflow上得到帮助后,我能够获得以下简单GUI倒计时的工作代码(仅显示一个窗口倒计时秒数)。 我对这个代码的主要问题是invokeLater部分不太理解。
据我了解,invokeLater会将任务发送给事件调度线程(EDT),然后EDT会在“可以”时执行此任务。这是正确的吗?
据我理解,代码的工作方式如下:
1.在main方法中,我们使用invokeLater显示窗口(showGUI方法)。换句话说,显示窗口的代码将在EDT中执行。
2.在main方法中,我们还启动了counter,而且该计数器(按构造)在另一个线程中执行(因此不在事件调度线程中)。对吗?
3.counter在单独的线程中执行,定期调用updateGUIupdateGUI应该更新GUI。 而GUI在EDT中工作。 因此,updateGUI也应在EDT中执行。 这就是为什么updateGUI的代码被包含在invokeLater中的原因。这是正确的吗?
不清楚的是为什么要从EDT调用counter。无论如何,它不在EDT中执行。它会立即启动一个新线程,并在那里执行counter。 所以,为什么我们不能在invokeLater块之后的主方法中调用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 这里有一个更详细的讨论:https://dev59.com/7nVC5IYBdhLWcg3wykaj - Kiril
@Roman 注意: 你的计数器不是从EDT开始,而是从主线程开始。计数器通过updateGUI方法更新GUI,在EDT上进行更新(因为调用了invokeLater)。 - Kiril
4个回答

16

如果我正确理解了你的问题,你想知道为什么你不能这样做:

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
  • counter是一个独立的线程,依赖于存在JLabel,以便它可以调用label.setText("You have " + i + " seconds.");
现在你有了竞争条件:必须在counter线程启动之前创建JLabel,如果没有在计数器线程启动之前创建,那么你的计数器线程将在未初始化的对象上调用setText
为了确保消除竞争条件,我们必须保证执行顺序,并且一种方式是在同一个线程上顺序执行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


1
invokeLater不会启动新线程,而是将Runnable安排在现有的AWT事件分派线程中运行。 - Steve Kuo
@Steve 谢谢,我已经更正了这一行。如果你注意到最后一个问答段落中我复制了文档,它明确说明:“[invokeLater]会在AWT事件分派线程上异步执行doRun.run()。”无论如何,我的竞争条件评估都是正确的。 - Kiril
这里没有理由不能使用invokeAndWait,对吧?这会使当前线程等待,直到runnable被执行。 - Chris Dennett
@Chris 我认为原帖作者对于竞态条件的识别存在困难,但是这个问题有很多解决方案。 - Kiril
@Josmas 我知道这个问题很久以前就存在了,但我同意你的观点。即使在 counter.start()invokeLater() 调用之外被调用,updateGUI(在 counter 线程中调用)也将对 label.setText 进行 invokeLater() 调用。因此,这行代码将在调用 showGUI() 之后被调用。我有什么遗漏的吗? - Joshua Kravitz
显示剩余3条评论

2
您实际上是从EDT启动计数器线程。如果您在invokeLater块之后调用counter.start(),则计数器可能会在GUI变得可见之前开始运行。现在,因为您正在EDT中构建GUI,当计数器开始更新它时,GUI将不存在。幸运的是,您似乎正在将GUI更新转发到EDT,这是正确的。由于EventQueue是一个队列,第一个更新将在GUI构建完成后发生,因此不应该有任何原因导致此方法无法正常工作。但是,更新尚未可见的GUI有什么意义呢?

谢谢。你说的对,我明白了。所以,如果我在invokeLater之后延迟一段时间再开始计数器,它应该可以工作吗? - Roman
@Roman 不行,这仍然不会起作用...时间延迟并不能给你一个保证,它只是为你赢得了时间,但你仍然有竞争条件。你不想掩盖你的竞争条件,你想要消除它们。 - Kiril
Joonas Pulakka,但为什么它应该是双向的?你在哪里犯了错? - Roman
@Lirik:不,他没有。他在invokeLater块的run块内开始计数器,该块在EDT中执行。 - Joonas Pulakka
@Roman:现在我仔细看了一下,是的,我错了;如果在invokeLater之后启动counter,它将无法工作。但是原因是,当第一次调用updateGUI时,即使在EDT中执行了setTextlabel引用也是从计数器第一次调用updateGUI时获取的 - 也就是说,在构造updateGUIRunnable时,此时label仍然为空!无论如何,在这里更好的解决方案是使用invokeAndWait - Joonas Pulakka
显示剩余2条评论

1
EDT 是什么?
它是对 Swing API 中存在的众多并发问题的一种权宜之计 ;)
说真的,很多 Swing 组件都不是“线程安全的”(一些著名的程序员甚至称 Swing 为“线程敌对的”)。通过拥有一个唯一的线程,所有更新都在这个线程敌对组件上进行,你可以避免许多潜在的并发问题。此外,你还可以保证使用 invokeLater 传递的 Runnable 以顺序方式运行。
然后一些吹毛求疵的东西:
public static void main(String[] args) {
    SwingUtilities.invokeLater(new Runnable() {
        public void run() {
            showGUI();
            counter.start();
        }
    });
}

然后:

在主方法中,我们也启动计数器,计数器(通过构造)在另一个线程中执行(因此不在事件分派线程中)。对吗?

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

这可能有些吹毛求疵,但考虑到问题,这仍然很重要。


WizardOfOdds,我知道计数器不在EDT上运行。它在一个单独的线程中运行,该线程从EDT启动。我也知道计数器不在与主方法相同的线程中运行。我只是想说,主方法将showGUIcounter.start发送到EDT,而counter.start则从EDT启动一个新线程。 - Roman
@Roman:没错,完全正确。但是重要的是措辞要准确,以防其他人阅读这个问题/答案时出现误解 :) - SyntaxT3rr0r

0

这很简单,步骤如下:

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

第二步。创建一个可运行的对象并将其传递给invokeLate()。

第三步。这将初始化GUI但不创建GUI。

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

第五步。GUI已经创建。

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


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接