Java难绷知识05--Swing中的事件调度线程和资源释放

news/2025/1/4 12:38:13/文章来源:https://www.cnblogs.com/ErgouTree/p/18646109

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.invokeLaterSwingUtilities.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,欢迎来玩!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/862347.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

共享ubuntu系统宿主机的部分文件到win虚拟机--通过ISO文件挂载

安装genisoimage sudo apt-get update sudo apt-get install genisoimage将需要共享的文件放入指定文件夹 cp /path/to/your/file ~/iso_work/使用genisoimage生成新镜像 genisoimage -o /path/to/new.iso -J -R -V "NEW_ISO_LABEL" ~/iso_work/其中new.iso就是新镜像…

Luogu P9646 SNCPC2019 Paper-cutting 题解 [ 紫 ] [ manacher ] [ 贪心 ] [ 哈希 ] [ BFS ]

manacher 与贪心的好题。Paper-cutting:思维很好,但代码很构式的 manacher 题。 蒟蒻 2025 年切的第一道题,是个紫,并且基本独立想出的,特此纪念。 判断能否折叠 我们先考虑一部分能折叠需要满足什么条件。显然,这一部分需要是一个长度为偶数的回文串。 那么横向和纵向会…

深度学习基础理论————分布式训练(模型并行/数据并行/流水线并行/张量并行)

主要介绍Pytorch分布式训练代码以及原理以及一些简易的Demo代码 模型并行 是指将一个模型的不同部分(如层或子模块)分配到不同的设备上运行。它通常用于非常大的模型,这些模型无法完整地放入单个设备的内存中。在模型并行中,数据会顺序通过各个层,即一层处理完所有数据之后…

overleaf-Latex教程

1.领取免费服务器,推荐免费服务器(SanFengYun)见下图。2.安装宝塔面板,配置内网为127.0.0.1,访问外网地址。 3.可以在宝塔面板一键部署网站,输入自己的域名即可。 4.关键:安装docker,安装yum,设置github可以访问。 5.更换docker镜像,自带镜像无法访问 6.按照overleaf…

Sola的2024年度总结

前言 2024 这一年对我来说确实意义非凡,很想写点东西来记录一下这一年我的经历,算是第一次写年度总结了。 简短的记录一下我这一年。 现在?未来? 回忆起大一下最后一节体育课,体育老师让每个人想一个词来描述这个上半年,我给出的答案是 : 迷茫 。 现在来看,这个答案贯穿…

洛谷 P11487 「Cfz Round 5」Gnirts 10——题解

洛谷P11487「Cfz Round 5」Gnirts 10传送锚点摸鱼环节 「Cfz Round 5」Gnirts 10 题目背景 English statement. You must submit your code at the Chinese version of the statement.In Memory of \(\text{F}\rule{66.8px}{6.8px}\). 题目描述 题面还是简单一点好。给定 \(n, …

基于高德地图API在Python中实现地图功能的方法

本文介绍在高德开放平台中,申请、获取地图API的Key的方法;同时通过简单的Python代码,调取API信息,对所得Key的可用性加以验证~本文介绍在高德开放平台中,申请、获取地图API的Key的方法;同时通过简单的Python代码,调取API信息,对所得Key的可用性加以验证。首先,我们进入…

活动对象----active object

一.preface 近期学习QPC框架,其核心之一就是 actvie-object,活动对象的出现是为了解决并发(阻塞、数据竞争)问题。笔者这里做一篇笔记,方便日后回顾。 二.What is "active object"活动对象的组成框架代码如下点击查看代码 typedef struct Active Active; typedef …

新的一年,我决定拆解一个蓝牙接收器

哈哈,容我介绍一下,如果大家对电子感兴趣,可以看一下下面的图片,会经常更新优秀的原创文章。再次感谢每一个努力的电子爱好者。今天我们来拆解一个蓝牙接收器,首先我们需要有一个直观的印象。下图就是我们这次需要拆解的对象。我再想这么小的接收器,电路是怎么放进去得呢…

jfianl 如何定时某个时间点执行一个任务

如果我们需要在某个点执行一个任务,可以用使用以下方法,首先在操作之间先明白思路 参考技术来源:https://jfinal.com/doc/9-2 第一步,先安装包,因为 这是第三方包: <dependency> <groupId>it.sauronsoftware.cron4j</groupId> <artifactId>cr…

Window平台下Visual Studio版本和Qt构建kit 以及OpenCV的对应关系

1、VS版本、MSVC版本、工具集的对应关系 参考https://www.cnblogs.com/lidabo/p/183977552、Qt中的构建kit和MSVC的对应关系 qt中使用对应版本的kit必须安装对应版本的VS才能使用3、OpenCV的VC17文件夹和VS版本的对应关系 OpenCV中的VC17文件夹就是指用的VS2022编译的库,visua…

题解:AT_abc386_d [ABC386D] Diagonal Separation

分析题面,发现题目求的是是否存在一个白点被 \((1, 1)\) 和任意一个黑点围成的矩形内。 先将所有黑点按 \(x\) 坐标排序。 枚举所有的白点。 找到所有横坐标不比该白点横坐标小的所有黑点的纵坐标的最大值所属点。如果该点的纵坐标小于该白点的纵坐标:(蓝点代表题目中的白点…