事件派发线程如何工作?
在stackoverflow上的人的帮助下,我能够获得以下简单GUI倒计时的工作代码(它只显示一个倒数秒的窗口)。 我对这段代码的主要问题是invokeLater
东西。
据我了解invokeLater
,它向事件调度线程(EDT)发送任务,然后EDT在“可以”(无论这意味着什么)时执行此任务。 是对的吗?
根据我的理解,代码的工作原理如下:
-
在
main
方法中,我们使用invokeLater
来显示窗口(showGUI
方法)。 换句话说,显示窗口的代码将在EDT中执行。 -
在
main
方法中,我们也启动counter
,并且计数器(通过构造)在另一个线程中执行(因此它不在事件调度线程中)。 对? -
counter
在一个单独的线程中执行,并定期调用updateGUI
。updateGUI
应该更新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上执行的匿名Runnable
的run()方法中启动计数器。 所以你真的从EDT 开始计数器Thread
,而不是主要的方法。 然后,因为它是一个单独的线程,它不在EDT上运行 。 但是计数器肯定是在EDT上启动的,而不是在Thread
执行main(...)
方法。
这是挑剔,但我认为这个问题仍然很重要。
这很简单,如下
步骤1 。 初始线程也称为主线程被创建。
步骤2.创建一个可运行的对象并将其传递给invokeLate()。
步骤3.这初始化GUI但不创建GUI。
步骤4.InvokeLater()调度创建的对象以便在EDT上执行。
步骤5.已创建GUI。
步骤6.所有发生的事件都将放在EDT中。