事件派发线程如何工作?

在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在那里执行。 那么, 为什么我们不能在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(); } }); } } 

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

 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.");

现在你有一个竞争条件:必须在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(); 从同一个线程执行,因此将在counter启动之前创建JLabel

更新:

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

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

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

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

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

什么是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(...)方法。

这是挑剔,但我认为这个问题仍然很重要。

这很简单,如下

步骤1 。 初始线程也称为主线程被创建。

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

步骤3.这初始化GUI但不创建GUI。

步骤4.InvokeLater()调度创建的对象以便在EDT上执行。

步骤5.已创建GUI。

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