Swing中的事件调度线程
先了解一下Swing中的单线程模型
单线程模型有什么作用
虽然大伙认为Swing又丑又落后(但是我编写gui入门真的是从Swing开始)
Swing 最初设计是单线程模型,这意味着所有与 Swing 组件交互的代码都应该在同一个线程中执行。
单线程模型避免了 Swing 组件可能因为会多个线程同时访问和修改而导致数据不一致或界面闪退等问题
而且单线程模型使得开发者无需处理复杂的线程同步问题,这是保持页面一致性的重要原因,同一时间只有一个线程可以操作组件,正常思维去调度下不会出现部分界面更新而其他部分未更新的情况。
但是单线程模型会造成一个迟钝的API。为了达到单线程模型,有一个专门的线程用于和Swing组件交互,就是Swing事件调度线程(Event DispatchThread,EDT)。
如果对单线程模型线程不清楚,可能在打造响应式界面和其他更多的扩展应用上会出很多问题。
为什么要了解事件调度线程(EDT)
因为,EDT 是 Swing 单线程模型的核心
在Swing中,所有与 UI 相关的操作,如创建组件、修改组件属性、添加或移除组件等,都必须在 EDT 中执行,EDT 负责处理所有的 Swing 事件,但是如果长时间运行的任务或者带有阻塞机制的任务在EDT 中执行,会导致UI冻结,对EDT机制深入了解就会做出更正确的取舍。所以在Swing中执行耗时任务时,要在一个新线程中执行,不能阻塞EDT线程,否则会造成swing界面的不响应,那就卡死了。SwingWorker就是用来管理任务线程和EDT之间调度的一个工具类。在这里我们先不讲SwingWorker,因为这个东西我也不咋会。
并且Swing 组件不是线程安全的,这意味着如果在非 EDT 线程中更新 UI,会导致不可预测的行为(通常是卡死然后瞬间爆炸)
而且在后台中,通常会有其他线程去操作Swing,EDT机制就是后台线程操作 Swing 组件的特定机制,即保持了单线程模型的完整性,而且也能利用多线程的优势来提高应用程序的性能。
Swing中的三种线程
一个swing程序包含三种类型的线程:初始化线程(Initial Thread)、事件调度线程(Event Dispatch Thread)和任务线程(Worker Thread)。
初始化线程
初始化线程读取程序参数并初始化一些对象。该线程主要目的是启动程序的图形用户界面(GUI)。
初始化线程用于创建各种容器,组件并显示他们,一旦创建并显示,初始化线程的任务就结束了,程序的控制权就交给了UI。
初始化线程的在main方法中启动点z:main
方法在主线程中执行。这也是 Java 应用程序的入口点。
虽然主线程可以启动 Swing 应用程序,但直接在主线程中创建和操作 Swing 组件是不推荐的,因为这可能导致界面不响应或出现线程安全问题。通常,主线程会将 Swing 组件的创建和初始化工作委托给事件调度线程。
如代码所示,同时我们在初始化一个图形界面的时候,都会直接在主方法的主线程里,直接调用如下代码来进行初始化
new TestFrame().setVisible(true);
但是复杂的程序我不推荐这样处理,因为这里有两个线程在同时访问组件:1. 主线程 2. 事件调度线程。
如果是复杂的图形界面程序,就有可能出现这两个线程同时操作的情况,导致同步问题的产生。
所以说我们创建和显示界面的工作,最好也交给事件调度线程
SwingUtilities.invokeLater(new Runnable() {public void run() {new TestFrame().setVisible(true);}
});
事件调度线程
事件调度线程主要负责GUI组件的绘制和更新,并响应用户的输入。
大家学了Swing那肯定都学了事件监听,通过对事件监听的学习,我们了解到Swing是一个事件驱动的模型,所以说所有和事件相关的操作都放是放在事件调度线程 (Event Dispatch)中进行的。
在 Swing 应用程序启动时,EDT 会自动启动。一般通过SwingUtilities.invokeLater
方法将代码块提交到 EDT 执行。例如:
import javax.swing.*;
public class EDTExample {public static void main(String[] args) {SwingUtilities.invokeLater(() -> {JFrame frame = new JFrame("EDT Example");frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);frame.setSize(300, 200);frame.setVisible(true);});}
}
事件队列处理:
每个EDT都会负责管理一个事件队列,用户每次对界面更新的请求都会排到事件队列中,然后等待EDT的处理。
EDT 负责从一个特定的事件队列中取出事件,并将其分发给相应的 Swing 组件进行处理。当用户与 Swing 界面进行交互时,比如点击按钮、移动鼠标、输入键盘字符等操作,都会生成相应的事件对象,这些对象会被放入事件队列。EDT 不断循环,从队列中取出事件,并调用组件注册的事件监听器中的对应方法
由于 EDT 是单线程处理事件,它保证了事件处理的顺序性。避免了多线程并发访问导致的不一致性和错误。
保证Swing组件的线程安全
单线程操作模型:Swing 组件并非线程安全,这意味着如果多个线程同时尝试访问和修改同一个 Swing 组件,可能会导致数据不一致、界面显示异常甚至程序崩溃等问题。EDT 通过将所有与 Swing 组件的交互操作限制在单个线程内执行,有效地避免了这些线程安全问题。
协调 UI 更新:所有对 Swing 组件可视化属性的修改,如改变组件的大小、位置、颜色等,都必须在 EDT 中进行。这样可以保证在任何时刻,组件的状态都是可预测和一致的。
import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;public class SingleThreadModelExample {private static JLabel label;public static void main(String[] args) {SwingUtilities.invokeLater(() -> {JFrame frame = new JFrame("Single - Thread Model Example");frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);frame.setSize(300, 200);frame.setLayout(new FlowLayout());JButton button = new JButton("Start Bad Thread");label = new JLabel("Initial Text");button.addActionListener(new ActionListener() {@Overridepublic void actionPerformed(ActionEvent e) {// 错误做法:在非EDT线程中尝试更新Swing组件// 在非 EDT 线程中更新 Swing 组件可能出现问题Thread badThread = new Thread(() -> {// 这会导致异常,因为Swing组件不是线程安全的,不能在非EDT线程中更新// label.setText("This will cause an issue");// 正确做法:使用SwingUtilities.invokeLater在EDT中更新SwingUtilities.invokeLater(() -> {label.setText("Updated in EDT");});});badThread.start();}});frame.add(button);frame.add(label);frame.setVisible(true);});}
}
任务线程
在上面我们一直在说,有阻塞能力或者耗时长的操作中我们不放在事件调度线程中执行,那么就放在任务线程
任务线程用于执行耗时操作如网络连接、文件读写、复杂计算等,以避免阻塞 EDT,保证 Swing 应用程序的界面始终保持响应性。
这些操作一般都会在事件响应后发起,就会自动进入事件调度线程。 而事件调度线程又是单线程模式,其结果就会是在执行这些长耗时任务的时候,界面就无响应了。
工作者线程在完成任务后,如果需要更新 Swing 组件,不能直接操作,而是要通过SwingUtilities.invokeLater
方法将更新操作提交到 EDT。
import javax.swing.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;public class WorkerThreadExample {private static JLabel label;public static void main(String[] args) {SwingUtilities.invokeLater(() -> {JFrame frame = new JFrame("Worker Thread Example");frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);frame.setSize(300, 200);frame.setLayout(new java.awt.FlowLayout());// 点击按钮启动工作者线程,工作者线程完成模拟耗时操作后,通过SwingUtilities.invokeLater方法在 EDT 中更新标签的文本。JButton button = new JButton("Start Worker");label = new JLabel("Status: Not started");frame.add(button);frame.add(label);frame.setVisible(true);button.addActionListener(new ActionListener() {@Overridepublic void actionPerformed(ActionEvent e) {new Thread(() -> {// 模拟耗时操作try {Thread.sleep(3000);} catch (InterruptedException ex) {ex.printStackTrace();}// 更新UI,必须在EDT中执行SwingUtilities.invokeLater(() -> {label.setText("Status: Completed");});}).start();}});});}
}
EDT线程的注意事项和细节
事件调度线程的单线程的
始终记住事件调度线程是单线程的。
这是因为 Swing里面的各种组件类都不是线程安全的,这就意味着,如果有多个线程,那么同一个组件的方法可能会被多个线程同时调用,这会导致同步问题以及错误数据的发生。
为了规避同步问题,以及降低整个Swing设计的复杂度,提高Swing的相应速度,Swing中的 事件调度线程被设计成为了单线程模式,即只有一个线程在负责事件的响应工作。
EDT 的启动
在 Swing 应用程序启动时,EDT 会自动启动。当调用SwingUtilities.invokeLater或SwingUtilities.invokeAndWait时,实际上是将任务提交到 EDT 的事件队列中。
任何GUI的请求都必须由EDT线程来处理
保证线程安全
Swing 组件不是线程安全的。如果在非 EDT 线程中执行 UI 相关操作,会导致不可预测的行为。
使用SwingUtilities.invokeLater(Runnable doRun)
方法将 UI 操作代码封装在Runnable
对象中提交给 EDT 执行。
SwingUtilities.invokeLater(() -> {JLabel label = new JLabel("New Label");frame.add(label);frame.revalidate();frame.repaint();
});
EDT线程将所有的GUI组件绘制和更新请求以及事件请求都放入了一个事件队列中。通过事件队列的机制,就可以将并发的GUI请求转化为事件队列,从而按顺序处理,这样有效的保护了线程安全,所以说,尽管大多数swing API本身不是线程安全的,但是swing通过EDT线程和事件队列机制实现了保障线程安全。
同理,不建议从其他线程直接访问UI组件及其事件处理器,这会破坏线程安全的保障,可能会导致界面更新和绘制错误。
在非EDT线程中通过invokeLater和invokeAndWait方法向EDT线程的事件队列添加GUI请求
有的时候需要在一个非EDT线程中调用swing API来处理GUI请求,显然我们不能直接访问GUI组件,就需要使用SwingUtilities.invokeLater
和SwingUtilities.invokeAndWait
方法向 EDT 的事件队列添加 GUI 请求。
通过invokeLater和invoke方法,可以从一个非EDT线程中,将GUI请求添加到EDT线程的事件队列中去。
invokeLater是异步的,调用该方法时,该方法将GUI请求添加到事件队列中后直接返回。InvokeAndWait是同步的,调用该方法时,该方法将GUI请求添加到事件队列中后,会一直阻塞,直到该请求被完成后才会返回。
但是在 EDT 线程中调用invokeAndWait
可能会导致死锁,例如,如果 EDT 在等待另一个线程释放资源,而这个线程又在等待 EDT 执行invokeAndWait
提交的任务,就会形成死锁。而且invokeAndWait
会阻塞调用线程,可能会影响程序的整体性能。因此,要避免在 EDT 中调用invokeAndWait
。
下面简单介绍这两个方法:
SwingUtilities.invokeLater
-
invokeLater
方法用于将一个Runnable
任务添加到 EDT 的事件队列末尾。EDT 会在处理完当前队列中的所有事件后,尽快执行这个任务。这意味着提交的任务不会立即执行,而是在 EDT 有空闲时才会被处理。 -
适用于那些对执行时机要求不是特别严格的 GUI 更新操作。
import javax.swing.*; import java.awt.*; import java.awt.event.ActionEvent; import java.awt.event.ActionListener;public class InvokeLaterExample {private static JLabel label;public static void main(String[] args) {SwingUtilities.invokeLater(() -> {JFrame frame = new JFrame("InvokeLater Example");frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);frame.setSize(300, 200);frame.setLayout(new FlowLayout());JButton button = new JButton("Start Thread");label = new JLabel("Initial Text");button.addActionListener(new ActionListener() {@Overridepublic void actionPerformed(ActionEvent e) {new Thread(() -> {// 模拟后台任务try {// 模拟一个耗时 2 秒的任务Thread.sleep(2000);} catch (InterruptedException ex) {ex.printStackTrace();}// 任务完成后,通过invokeLater方法将更新JLabel文本的操作添加到 EDT 的事件队列中SwingUtilities.invokeLater(() -> {label.setText("Text updated from background thread");});}).start();}});frame.add(button);frame.add(label);frame.setVisible(true);});} }
SwingUtilities.invokeAndWait
-
invokeAndWait
方法同样用于将一个Runnable
任务添加到 EDT 的事件队列,但与invokeLater
不同的是,调用invokeAndWait
的线程会阻塞,直到 EDT 执行完提交的任务。这确保了调用线程可以立即获取到任务执行的结果 -
当非 EDT 线程需要依赖 GUI 操作的结果继续执行后续逻辑时,适合使用
invokeAndWait
。```Java
import javax.swing.;
import java.awt.;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
public class InvokeAndWaitExample {
private static JTextField textField;
public static void main(String[] args) {SwingUtilities.invokeLater(() -> {JFrame frame = new JFrame("InvokeAndWait Example");frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);frame.setSize(300, 200);frame.setLayout(new FlowLayout());JButton button = new JButton("Get Text");textField = new JTextField(10);button.addActionListener(new ActionListener() {@Overridepublic void actionPerformed(ActionEvent e) {new Thread(() -> {try {// 后台线程通过invokeAndWait获取JTextField中的文本String text = SwingUtilities.invokeAndWait(() -> {return textField.getText();});// 由于invokeAndWait会阻塞后台线程,直到 EDT 执行完获取文本的任务,所以可以确保获取到准确的文本值并显示在对话框中。JOptionPane.showMessageDialog(frame, "Text from field: " + text);} catch (Exception ex) {ex.printStackTrace();}}).start();}});frame.add(textField);frame.add(button);frame.setVisible(true);});
}
避免在 EDT 中执行耗时操作
EDT 负责处理所有的 Swing 事件和 UI 更新。
EDT的事件队列的机制在保障了线程安全的同时,也引入了一个新的问题:假设事件队列中某一个GUI请求执行时间非常长,那么由于队列的特点,队列中的后续GUI请求都会被阻塞,导致界面无法响应用户输入,出现界面冻结的情况。
考虑到用户体验性,应使用独立的任务线程来执行耗时计算或输入输出密集型任务
所以,将耗时操作放在任务线程中执行,在任务线程完成任务后,如果需要更新 UI,再通过SwingUtilities.invokeLater
将更新操作提交到 EDT。
调试 EDT 相关问题
检测跨线程操作:使用工具如 Java VisualVM 或 Eclipse 的调试工具,可以检测是否存在在非 EDT 线程中访问 Swing 组件的情况
监控 EDT 性能:如果怀疑 EDT 出现性能问题(如界面响应缓慢),可以通过分析事件处理代码的执行时间,查找是否存在耗时操作在 EDT 中执行。可以使用简单的日志记录或性能分析工具( YourKit Java Profiler)来辅助诊断。
上一篇:Java难绷知识03--异常处理中的finally块
下一篇:Java难绷知识06——Scanner等输出的细节
文章个人编辑肯定会有各种欠缺和漏洞,需要大家积极反馈来帮助这篇文章和我的技术知识的更进一步,也有不合理的地方需要大家指出,感谢每一位读者
QQ:1746928194,是喜欢画画的coder,欢迎来玩!