实用指南:Java多线程编程【SwingUtilities类】与【UI多线程编程】

第二十章 多线程编程

20.6 SwingUtilities类与UI多线程编程

20.6.1 SwingUtilities类

SwingUtilities是Java Swing工具包中的一个实用工具类,提供了一系列静态方法来帮助开发GUI应用程序,尤其是在处理多线程与Swing组件的交互时。
SwingUtilities类从接口javax.swing.SwingConstants 继承常量字段。
SwingUtilities类的继承层次图:
在这里插入图片描述

从Java 6 开始,许多SwingUtilities方法也被复制到了SwingWorker类中。
在Swing图形界面程序里,常有一个顶级框架容器,如JFrame或JDialog实例,负责启动一个EventDispatchThread(事件分派线程,简称EDT),这是个单线程,这个线程负责处理GUI(图形用户界面)事件的维护。

首先,图形界面的Swing组件向EDT的EventQueue(事件队列)提交一个event(事件),由EDT负责调度各个event事件的派发,以及回调处理。
例如,点击按钮时,JButton向EDT线程的EventQueue提交一个ActionEvent事件。EDT线程根据调度算法轮到执行该ActionEvent时,会把ActionEvent派发给JButton上注册的监听器,监听器再调用事件处理器(actionPerformed)进行处理,这个过程并没有创建新线程,事件处理器是在EDT线程内完成的。

理解SwingUtilities类作用的前提是先理解事件派发线程的概念。
当运行一个 Swing 图形界面程序时,Java虚拟机(JVM)会自动创建三个线程:
(1)主线程:main线程。main方法作为程序的入口,该线程主要作用是初始化程序环境,并启动程序的GUI(图形用户界面)。
(2)Toolkit 线程:负责捕捉系统事件,比如键盘事件、鼠标移动、窗口移动和缩放等,程序员编写的任何代码不会在这个线程上执行。Toolkit线程的作用是把捕获的事件Event传递给EDT线程(事件派发线程)。
(3)EDT线程(Event Dispatcher Thread):事件派发线程。EDT线程接收Toolkit线程传递来的Event事件,并将事件插入事件队列(EventQueue)中。它根据调度算法来派发事件,把队列中的event(事件)派发给事件监听器,事件监听器调用事件处理器回调函数来处理。所有的事件处理代码并非在主线程main中处理,而是在EDT线程中执行的。一个应用程序有且只有一个EDT线程。

由于EDT线程是单线程操作,所以只有等前面事件处理器的回调函数执行完毕后,才能执行GUI组件更新操作,然后周而复始继续派发后面的事件。

所以,如果我们在任何ActionListener、MouseListener等监听器对象中编写耗时的事务处理逻辑,整个GUI应用系统就会响应迟钝,甚至不响应。如果在监听器中执行系统调用wait(),以等待另一个线程锁定的资源或计算结果,EDT就会被阻塞,GUI界面就会因此停住,出现应用程序无响应的现象。

解决策略是:把事件处理器中耗时的操作分离出来,单独放到一个新线程(称为任务线程或工作线程)中执行,而不是让其在EDT线程中执行。

Swing应用程序中的线程大致可分为三种类型:
初始化线程(Initial Thread):比如main线程,该线程主要作用是初始化程序环境,并启动程序的GUI(图形用户界面)。
事件调度派发线程(EDT):Swing程序中只有一个EDT线程,它不但负责事件的派发、调用事件处理器来响应用户的请求;而且负责GUI组件的绘制和更新。程序中所有的事件处理都是由EDT分派和回调执行的,应用程序与GUI组件及其基本数据模型的交互只允许在EDT线程上进行。
任务线程(Worker Thread):EDT线程上不能运行耗时的任务,以便UI及时响应用户请求和操作。耗时的工作任务要放到任务线程中处理。

SwingUtilities主要功能:

(一)、线程相关方法:
invokeAndWait(Runnable doRun): 它可将任务线程(Runnable对象)添加到EDT线程的事件队列中同步执行,调用线程会阻塞直到任务完成。常用于需要取得Swing组件状态数据的情形。
invokeLater(Runnable doRun): 它可将任务线程(Runnable对象)添加到EDT线程的事件队列中,由EDT线程调度执行。但它与invokeAndWait方法不同,这是个异步方法。调用线程会立即返回。
isEventDispatchThread(): 检查当前线程是否是事件调度线程。

(二)、组件操作方法:
getRoot(Component c): 获取组件的顶级容器。
windowForComponent(Component c): 获取包含指定组件的窗口。
getAncestorOfClass(Class c, Component comp): 查找指定组件的特定类型祖先。

(三)、坐标转换方法:
convertPoint(Component source, Point aPoint, Component destination): 将点从一个组件的坐标系转换到另一个组件。
convertPointFromScreen(Point p, Component c): 将屏幕坐标转换为组件坐标。
convertPointToScreen(Point p, Component c): 将组件坐标转换为屏幕坐标。

(四)、其他实用方法:
isLeftMouseButton(MouseEvent anEvent): 检查是否是鼠标左键事件。
isRightMouseButton(MouseEvent anEvent): 检查是否是鼠标右键事件。
updateComponentTreeUI(Component c): 更新组件及其所有子组件的UI。

【例程13-6】 图像拖动例程DrageImageFrm
下面是一个图像拖动例程DrageImageFrm.java,图像随鼠标移动。这个例程演示了转换坐标系统方法convertPoint(Component source,Point point,Component dest)的用法。如果不进行坐标系统转换,鼠标移动时虽然图像也会跟随,但有点飘忽不定,图像与鼠标有时会有很大的间距,效果不理想。

package swingUtil;
//图像拖动例程DrageImageFrm.java开始:
import java.awt.Color;
import java.awt.Point;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import javax.swing.*;
/***
* @author QiuGen
* @description 图像拖动DragImageFrm
* 实现功能:拖动图像时,图像随鼠标移动
* @date 2024/2/16
* ***/
public class DragImageFrm extends JFrame {
private JLabel img;
//图像,用于演示拖动
public DragImageFrm() {
setTitle("图像的拖动");
img = new JLabel(new ImageIcon("Image\\红帅.GIF"));
JPanel panel=new JPanel();
panel.add(img);
//鼠标事件监听器
MouseAdapter listener=new MyMouseListener();
img.addMouseListener(listener);
//注册标签的事件监听器
img.addMouseMotionListener(listener);
//注册事件监听器 
add(panel);
setSize(360,200);
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
setVisible(true);
}
class MyMouseListener extends MouseAdapter{
Point point = null;
//原坐标点
Point newPoint = null;
//新坐标点
public void mousePressed(MouseEvent e)
{
//转换坐标系,获取当前坐标点。不能直接point=e.getPoint();
point=SwingUtilities.convertPoint(img,e.getPoint(),img.getParent());
}
public void mouseDragged(MouseEvent e)
{
//转换坐标系,获取新的坐标点。不能直接newPoint=e.getPoint(); 
newPoint=SwingUtilities.convertPoint(img,e.getPoint(),img.getParent());
//设置图像的新位置 
img.setLocation(img.getX()+(newPoint.x-point.x),img.getY()+(newPoint.y-point.y));
point=newPoint;
//更改坐标点
}
}
public static void main(String[] args) {
new DragImageFrm();
}
} // 图像拖动例程DrageImageFrm.java结束。

例程的测试效果图如下:
在这里插入图片描述

GUI多线程编程规则(注意事项):
(1)Swing组件不是线程安全的,GUI应用程序所有对Swing组件的访问都应在事件调度线程(EDT)上进行,invokeLater()和invokeAndWait()是确保线程安全的关键方法。由EDT线程统一处理Swing组件绘制和更新可避免并发问题;其他线程直接更新Swing组件会出现错误。

  1. invokeAndWait(runnable)
    用途:与 invokeLater方法 类似,它可将 Runnable 对象添加到EDT中。
    但这是同步执行的方法,线程调用该方法时,当前的调用线程将立即阻塞,直等到EDT处理完它的请求,调用线程才会继续执行后续代码。常用于需要取得Swing组件状态数据的情形。

  2. invokeLater(runnable)
    用途:将 Runnable 对象添加到EDT的事件队列中,待EDT处理完其他事件后再执行。与invokeAndWait方法不同,它是个异步方法。调用线程会立即返回。
    场景:当一个线程需要更新GUI时,可以调用 invokeLater 把更新操作委托给EDT执行
    样例代码:

SwingUtilities.invokeLater(new Runnable() {
@Override
public void run()
{
myTree.RefreshNode();
}
});

(2)不能在EDT线程内调用invokeAndWait,会引起死锁,导致程序崩溃。
可利用SwingUtilities.isEventDispatchThread()方法判断当前线程是否是EDT线程。样例代码:

if(SwingUtilities.isEventDispatchThread()) //是EDT线程
{
myTree.RefreshNode();
}
else //不是EDT线程
{
SwingUtilities.invokeAndWait(new Runnable()
{
@Override
public void run()
{
myTree.RefreshNode();
}
});
}

(3)禁止在EDT线程中执行耗时任务。
(4)耗时的任务放到独立的任务线程中执行。

EDT线程除了派发处理各种事件(Event)外,还能处理Runnnable对象UI组件绘制和更新事务。当任务线程需要更新Swing组件时,可以通过调用静态方法invokeAndWait()或invokeLater()把更新Swing组件的操作请求封装成Runnnable对象,插入EDT线程的EventQueue队列。
有两种调用静态方法方式,可任选一种,都是正确的,以invokeLater为例:
通过SwingUtilities类调用:SwingUtilities.invokeLater
通过EventQueue类调用:EventQueue.invokeLater

实际上,SwingUtilities版本的invokeLater()只是做了简单的封装,在其方法内部直接调用EventQueue.invokeLater()。因为Swing框架本身经常调用SwingUtilities,使用SwingUtilities可以减少程序引入的类。SwingUtilities版本invokeLater()的源代码如下:

public static void invokeLater(Runnable doRun) {
EventQueue.invokeLater(doRun);
} //源代码结束。

遵循GUI多线程编程规则可以避免许多常见的多线程问题,如数据不一致、程序不响应、非预期不确定的行为等。使用 SwingUtilities 类是改善这些问题的关键工具之一。

典型启动框架窗口问题

在Java典型的Swing应用程序中main()方法作为程序的入口,该线程是初始化线程,主要作用是初始化程序环境,并启动程序的GUI(图形用户界面)。main线程GUI(图形用户界面)启动后,这也是应用程序将控制权转交EDT线程的地方,往往也是同EDT交互出现问题的地方。

许多Swing应用程序使用下面方式启动程序,但这样写有BUG的,是错误的写法:

public class MainFrame extends javax.swing.JFrame {public static void main(String[] args)
{
new MainFrame().setVisible(true);
}
}

尽管在我们写简短的学习例程时,由于图形界面处理代码很少,问题虽然不会马上暴露出来,几乎发现不了异常。但是我们还是要注意避免这样书写,或者说至少要知道所以然。
正确启动GUI图形用户界面的正确写法如下:

public class MainFrame extends javax.swing.JFrame
{public static void main(String[] args) {
SwingUtilities.invokeLater(new Runnable() {
public void run() {
new MainFrame().setVisible(true);
}
});
}
}

20.6.2 使用SwingUtilities类完善UI多线程示例

这是一个使用Java Swing GUI和多线程技术的例程,主要实现了计数线程与用户界面交互的功能。

【例程20-7B】 多线程例程RunnableFrm的SwingUtilities完善版本

这是 【例程20-7A】 在程序主类中实现Runnable接口的例程RunnableFrm.java基础上利用SwingUtilities类进行完善,其功能与ThreadFrame.java相同。
程序实现原理分析:

  • 多线程架构:
    主线程:Swing事件分发线程(EDT),负责UI渲染和事件处理
    工作线程:thisThread,执行计数任务的独立线程

其使用的关键线程技术:

// 实现Runnable接口,使窗体本身可作为线程任务
public class RunnableFrm extends JFrame implements Runnable
// volatile关键字确保多线程间的可见性
private volatile boolean runFlg = true;
  • Swing UI组件布局
    布局管理器使用:
    (1)BorderLayout:主窗体布局
    (2)FlowLayout:控制面板布局
    (3)GridLayout:状态栏布局

UI组件界面的层次结构:
在这里插入图片描述

  • 线程安全机制
    SwingUtilities.invokeLater()
// 所有UI更新都在EDT中执行,避免线程冲突
SwingUtilities.invokeLater(new Runnable() {
public void run() {
// UI更新代码
}
});

完整的程序源代码,如下:

package thread;
import java.awt.BorderLayout;
import java.awt.Dimension;
import java.awt.FlowLayout;
import java.awt.GridLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import javax.swing.*;
import javax.swing.SwingUtilities;
import javax.swing.border.BevelBorder;
/***
* @author QiuGen
* @description 计数线程与录入例程RunnableFrm
* 实现功能:计数线程与文本录入,演示人机交互体验。
* @date 2024/5/24
* ***/
public class RunnableFrm extends JFrame implements Runnable {
private Thread thisThread = null;
private volatile boolean runFlg = true;
private int loopNum = 0;
// UI组件
private JTextArea textArea = new JTextArea(8, 30);
// 人工录入区域
private JTextField textField = new JTextField(10);
private JPanel controlPanel = new JPanel(new FlowLayout());
private JButton btnBegin = new JButton("开始");
private JButton btnSwitch = new JButton("切换");
// 切换焦点
private JButton btnStop = new JButton("停止");
private JButton btnClear = new JButton("清空录入");
// 状态栏组件
private JPanel statusPanel = new JPanel(new GridLayout(1, 2));
// 改为GridLayout平分空间
private JLabel statusLabel = new JLabel("就绪");
private JLabel counterLabel = new JLabel("计数: 0");
{
// 初始化块
// 设置文本区域属性(用于人工录入)
textArea.setLineWrap(true);
textArea.setWrapStyleWord(true);
textArea.setToolTipText("在此处输入文本内容");
// 控制面板
controlPanel.add(textField);
controlPanel.add(btnBegin);
controlPanel.add(btnSwitch);
controlPanel.add(btnStop);
controlPanel.add(btnClear);
// 状态栏设置 - 使用GridLayout平分空间
statusPanel.setBorder(new BevelBorder(BevelBorder.LOWERED));
statusPanel.setPreferredSize(new Dimension(getWidth(), 24));
// 添加标签到状态面板
statusPanel.add(statusLabel);
statusPanel.add(counterLabel);
// 添加事件监听器
btnBegin.addActionListener(new BtnBeginLn());
btnSwitch.addActionListener(new BtnSwitchLn());
btnStop.addActionListener(new BtnStopLn());
btnClear.addActionListener(new BtnClearLn());
}
public RunnableFrm() {
setLayout(new BorderLayout());
// 顶部控制面板
add(controlPanel, BorderLayout.NORTH);
// 中部文本录入区域(带滚动条)
add(new JScrollPane(textArea), BorderLayout.CENTER);
// 底部状态栏
add(statusPanel, BorderLayout.SOUTH);
setTitle("计数线程与录入操作");
setSize(500, 260);
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
setLocationRelativeTo(null);
// 窗口关闭监听器
addWindowListener(new WindowAdapter() {
@Override
public void windowClosing(WindowEvent e) {
stopThread();
}
});
setVisible(true);
}
/** 开始按钮监听器 */
private class BtnBeginLn implements ActionListener {
public void actionPerformed(ActionEvent e) {
if (thisThread == null || !thisThread.isAlive()) {
runFlg = true;
loopNum = 0;
thisThread = new Thread(RunnableFrm.this);
thisThread.start();
updateButtonStates(false, true);
updateStatus("线程已启动", "计数: 0");
}
}
}
/** 切换按钮监听器 - 切换焦点到文本区域 */
private class BtnSwitchLn implements ActionListener {
public void actionPerformed(ActionEvent e) {
textArea.requestFocusInWindow();
updateStatus("焦点已切换到文本录入区域", null);
textArea.setCaretPosition(textArea.getText().length());
}
}
/** 停止按钮监听器 */
private class BtnStopLn implements ActionListener {
public void actionPerformed(ActionEvent e) {
stopThread();
}
}
/** 清空按钮监听器 */
private class BtnClearLn implements ActionListener {
public void actionPerformed(ActionEvent e) {
textArea.setText("");
updateStatus("录入内容已清空", null);
textArea.requestFocusInWindow();
}
}
/** 安全停止线程 */
private void stopThread() {
if (thisThread != null && thisThread.isAlive()) {
thisThread.interrupt();
try {
thisThread.join(1000);
} catch (InterruptedException ex) {
Thread.currentThread().interrupt();
}
thisThread = null;
updateButtonStates(true, false);
updateStatus("线程已停止", "计数: " + loopNum);
}
}
/** 更新按钮状态 */
private void updateButtonStates(final boolean beginEnabled, final boolean stopEnabled) {
SwingUtilities.invokeLater(new Runnable() {
public void run() {
btnBegin.setEnabled(beginEnabled);
btnStop.setEnabled(stopEnabled);
}
});
}
/** 更新状态栏信息 */
private void updateStatus(final String message, final String counterText) {
SwingUtilities.invokeLater(new Runnable() {
public void run() {
if (message != null) {
statusLabel.setText(" " + message);
// 添加空格使文本不贴边
}
if (counterText != null) {
counterLabel.setText(counterText + " ");
// 添加空格使文本不贴边
}
}
});
}
/** 获取录入的文本内容 */
public String getInputText() {
return textArea.getText();
}
/** 清空录入区域 */
public void clearInputText() {
textArea.setText("");
}
@Override
public void run() {
updateStatus("计数线程开始执行", "计数: 0");
try {
for (; loopNum <
120 &&
!Thread.currentThread().isInterrupted();
) {
if (runFlg) {
final int currentNum = loopNum++;
// 安全更新UI
SwingUtilities.invokeLater(new Runnable() {
public void run() {
textField.setText(Integer.toString(currentNum));
counterLabel.setText("计数: " + currentNum + " ");
}
});
// 每计数10次更新一次状态
if (currentNum % 10 == 0) {
updateStatus("计数线程正在运行... ", null);
}
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
break;
}
}
} finally {
SwingUtilities.invokeLater(new Runnable() {
public void run() {
updateButtonStates(true, false);
}
});
if (Thread.currentThread().isInterrupted()) {
updateStatus("计数线程被中断", "最终计数: " + loopNum);
} else {
updateStatus("计数线程执行完成", "最终计数: " + loopNum);
}
}
}
public static void main(String[] args) {
SwingUtilities.invokeLater(new Runnable() {
public void run() {
new RunnableFrm();
}
});
}
}

由于 Swing组件对中文输入法有点不兼容,会引起录入中文时程序不响应。除了第一行英文是直接输入的,其余信息复制自文本编辑器。
例程的测试结果图:
在这里插入图片描述
这个例程很好地展示了Java Swing程序中如何正确处理多线程与UI的交互,是一个典型的生产者-消费者模式在GUI中的应用。

posted on 2025-09-18 08:52  ljbguanli  阅读(40)  评论(0)    收藏  举报