首页 > 编程语言 >Java难绷知识05--Swing中的事件调度线程和资源释放

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

时间:2025-01-01 17:29:32浏览次数:1  
标签:Java 05 -- frame 线程 EDT Swing 组件 new

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() {
                @Override
                public 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() {
                @Override
                public 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() {
                    @Override
                    public 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() {
            @Override
            public 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,欢迎来玩!

标签:Java,05,--,frame,线程,EDT,Swing,组件,new
From: https://www.cnblogs.com/ErgouTree/p/18646109

相关文章

  • Spring Data REST 远程代码执行漏洞(CVE-2017-8046)分析与复现4
    前言2009年9月Spring3.0RC1发布后,Spring就引入了SpEL(SpringExpressionLanguage)。对于开发者而言,引入新的工具显然是令人兴奋的,但是对于运维人员,也许是噩耗的开始。类比Struts2框架,会发现绝大部分的安全漏洞都和ognl脱不了干系。尤其是远程命令执行漏洞,占据了多少甲方乙方......
  • 数据结构与算法Python版 拓扑排序与强连通分支
    文章目录一、图的应用-拓扑排序二、图的应用-强连通分支一、图的应用-拓扑排序拓扑排序TopologicalSort从工作流程图得到工作次序排列的算法,称为“拓扑排序”拓扑排序处理一个有向无环图DAG,输出顶点的线性序列。使得两个顶点v,w,如果图中有(v,w)边,在线性序列中v就......
  • 函数递归与栈帧的创建与销毁
    目录函数递归函数栈帧的创建与销毁概述 main函数栈帧的创建变量的创建如何传参子函数栈帧的创建函数如何返回值(1)子函数栈帧的销毁函数如何返回值(2)函数递归将复杂的问题层层化为与原问题相似的规模较小的问题。递----递推、归----回归 递推:函数一直......
  • 进 来 秒 懂 指 针
    理清二维数组的指针各种表达方式的思维导图目录​编辑内存编址指针的类型指针的解引用危险的野指针成因如何规避野指针指针运算一维数组的指针二维数组的指针内存内存是电脑上的存储设备,程序运行时会加载到内存。任务管理器可以查看内存使用情况:内存......
  • 小程序组件 —— 23 组件案例 - 轮播图图片添加
    上一节实现了轮播图的最外层结构,但是没有通过轮播图来渲染图片,这一节我们先讲一下小程序中怎么来渲染图片,讲解完之后会通过轮播图来展示图片;在微信小程序中,如果需要渲染图片,需要使用image组件,常用的属性有4个:src属性:图片资源地址;mode:图片裁剪、缩放的模式;show-menu-b......
  • 小程序组件 —— 22 组件案例 - 轮播区域绘制
    这一节我们实现轮播图最外层的盒子,也就是把轮播图的最外层搭好,先不给轮播图添加图片,因为图片属于新的组件,组件里面有一些知识点,需要单独分开讲;回顾一下,在进行传统网页开发时,实现轮播图的时候,我们首先使用html、css实现轮播图的结构样式,然后使用JS控制轮播图的效果,或者......
  • 工学云智能打卡,异地签到
    在当今快节奏的学习和工作环境中,如何高效管理时间和优化流程成为许多职场人士和学生共同面临的挑战。为此,我们团队开发了一款专为工学云app设计的云打卡系统,该系统通过智能定位技术和GPT4技术的应用,旨在彻底改变传统打卡和报告撰写的方式,为用户提供更便捷、高效的学习与工作体......
  • 人工智能短视频内容理解与生成技术在美团的创新实践15
     1.背景美团围绕丰富的本地生活服务电商场景,积累了丰富的视频数据。美团场景下的短视频示例上面展示了美团业务场景下的一个菜品评论示例。可以看到,视频相较于文本和图像可以提供更加丰富的信息,创意菜“冰与火之歌”中火焰与巧克力和冰淇淋的动态交互,通过短视频形式进......
  • Spring Boot引起的“堆外内存泄漏”排查及经验总结10
    背景为了更好地实现对项目的管理,我们将组内一个项目迁移到MDP框架(基于SpringBoot),随后我们就发现系统会频繁报出Swap区域使用量过高的异常。笔者被叫去帮忙查看原因,发现配置了4G堆内内存,但是实际使用的物理内存竟然高达7G,确实不正常。JVM参数配置是“-XX:MetaspaceSize=256M-......
  • LruCache在美团DSP系统中的应用演进9
     背景DSP系统是互联网广告需求方平台,用于承接媒体流量,投放广告。业务特点是并发度高,平均响应低(百毫秒)。为了能够有效提高DSP系统的性能,美团平台引入了一种带有清退机制的缓存结构LruCache(LeastRecentlyUsedCache),在目前的DSP系统中,使用LruCache+键值存储数据库的机制......