第10章:线程与 SWT - 不要阻塞 UI
作者:步子哥 (steper@foxmail.com)
10.1 SWT 的线程规则
为什么只能在主线程操作 UI?
// 错误示例:在后台线程操作 UI
new Thread(() -> {
button.setText("后台线程修改"); // 错误!会抛出异常
}).start();
// 正确示例:在主线程操作 UI
display.asyncExec(() -> {
button.setText("主线程修改"); // 正确!
});
费曼解释:为什么不能跨线程操作 UI?
UI 线程 = 舞台上的演员
- 只有一个演员在台上(UI 线程)
- 后台线程 = 后台工作人员
- 后台工作人员不能直接上台表演
- 必须通过"导演"(display.asyncExec)传递任务
类比:
UI 线程 = 餐厅前台
- 前台服务员(UI 线程)直接服务顾客
- 后台厨师(后台线程)不能直接服务顾客
- 必须通过"传菜员"(asyncExec)把菜传给前台
跨线程访问 UI 的后果
// 错误示例:在后台线程修改 UI
new Thread(() -> {
try {
Thread.sleep(1000); // 模拟耗时操作
button.setText("后台线程修改"); // 抛出异常!
} catch (Exception e) {
e.printStackTrace();
}
}).start();
异常信息:
org.eclipse.swt.SWTException: Invalid thread access
费曼解释:为什么会抛出异常?
跨线程访问 UI = 越权
- UI 线程有"权限"操作 UI
- 后台线程没有"权限"
- 后台线程强行操作 UI = 越权
- 抛出异常 = 被保安抓住
类比:
跨线程访问 UI = 擅自进入后台
- 前台(UI 线程):顾客可以进入
- 后台:只有员工可以进入
- 顾客(后台线程)擅自进入后台 = 违规
- 被保安抓住 = 抛出异常
代码示例:线程规则演示
public class ThreadRuleExample {
public static void main(String[] args) {
Display display = new Display();
Shell shell = new Shell(display);
shell.setText("线程规则示例");
shell.setLayout(new GridLayout(1, false));
// 按钮面板
Composite buttonPanel = new Composite(shell, SWT.NONE);
buttonPanel.setLayout(new FillLayout(SWT.HORIZONTAL));
// 错误按钮:后台线程直接修改 UI
Button wrongButton = new Button(buttonPanel, SWT.PUSH);
wrongButton.setText("错误:后台线程修改");
wrongButton.addListener(SWT.Selection, event -> {
new Thread(() -> {
try {
Thread.sleep(1000);
// 错误!后台线程不能直接修改 UI
wrongButton.setText("后台线程修改");
} catch (Exception e) {
e.printStackTrace();
display.asyncExec(() -> {
wrongButton.setText("错误:" + e.getMessage());
});
}
}).start();
});
// 正确按钮:通过 asyncExec 修改 UI
Button correctButton = new Button(buttonPanel, SWT.PUSH);
correctButton.setText("正确:asyncExec");
correctButton.addListener(SWT.Selection, event -> {
new Thread(() -> {
try {
Thread.sleep(1000);
// 正确!通过 asyncExec 修改 UI
display.asyncExec(() -> {
correctButton.setText("主线程修改");
});
} catch (Exception e) {
e.printStackTrace();
}
}).start();
});
// 文本框:显示日志
Text logText = new Text(shell, SWT.BORDER | SWT.MULTI | SWT.V_SCROLL);
GridData logData = new GridData();
logData.horizontalAlignment = GridData.FILL;
logData.verticalAlignment = GridData.FILL;
logData.grabExcessHorizontalSpace = true;
logData.grabExcessVerticalSpace = true;
logData.heightHint = 200;
logText.setLayoutData(logData);
// 添加日志的辅助方法
shell.getDisplay().asyncExec(() -> {
logText.append("主线程:" + Thread.currentThread().getName() + "\n");
});
shell.setBounds(100, 100, 600, 400);
shell.open();
while (!shell.isDisposed()) {
if (!display.readAndDispatch()) {
display.sleep();
}
}
display.dispose();
}
}
10.2 后台任务处理
创建工作线程
// 正确示例:后台线程处理任务,主线程更新 UI
Button button = new Button(shell, SWT.PUSH);
button.setText("开始任务");
button.addListener(SWT.Selection, event -> {
button.setEnabled(false); // 禁用按钮,防止重复点击
new Thread(() -> {
try {
// 后台线程:执行耗时任务
for (int i = 0; i <= 100; i++) {
final int progress = i;
// 主线程:更新 UI
display.asyncExec(() -> {
progressBar.setSelection(progress);
statusLabel.setText("进度:" + progress + "%");
});
Thread.sleep(50); // 模拟耗时操作
}
// 任务完成
display.asyncExec(() -> {
button.setEnabled(true); // 启用按钮
statusLabel.setText("任务完成!");
});
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
});
费曼解释:后台任务处理的流程
后台任务处理 = 餐厅点餐
1. 顾客(用户)点菜(点击按钮)
2. 前台服务员(UI 线程)把菜单给后厨(后台线程)
3. 后厨(后台线程)做菜(执行任务)
4. 做好后,后厨把菜给传菜员(asyncExec)
5. 传菜员把菜端给顾客(更新 UI)
6. 顾客享用美食(任务完成)
类比:
后台任务处理 = 快递
1. 你(用户)在淘宝下单(点击按钮)
2. 快递员(后台线程)取件(执行任务)
3. 快递员把包裹送到驿站(asyncExec)
4. 你去驿站取件(更新 UI)
5. 你收到包裹(任务完成)
避免 UI 卡顿
// 错误示例:在主线程执行耗时任务,UI 卡顿
Button wrongButton = new Button(shell, SWT.PUSH);
wrongButton.setText("错误:主线程执行耗时任务");
wrongButton.addListener(SWT.Selection, event -> {
// 错误!在主线程执行耗时任务,UI 卡顿
try {
for (int i = 0; i <= 100; i++) {
Thread.sleep(50); // 模拟耗时操作
progressBar.setSelection(i);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
});
// 正确示例:在后台线程执行耗时任务,UI 流畅
Button correctButton = new Button(shell, SWT.PUSH);
correctButton.setText("正确:后台线程执行耗时任务");
correctButton.addListener(SWT.Selection, event -> {
new Thread(() -> {
// 正确!在后台线程执行耗时任务,UI 流畅
try {
for (int i = 0; i <= 100; i++) {
final int progress = i;
display.asyncExec(() -> {
progressBar.setSelection(progress);
});
Thread.sleep(50);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
});
费曼解释:为什么后台线程可以避免 UI 卡顿?
UI 卡顿 = 独木桥
- 只有一个人(主线程)在桥上
- 如果这个人走得慢(执行耗时任务)
- 后面的人(UI 事件)都过不去
- UI 卡顿 = 人群拥堵
后台线程 = 架设新桥
- 原来一座桥(主线程):UI 事件
- 新建一座桥(后台线程):耗时任务
- 两座桥互不干扰
- UI 流畅 = 人群畅通
类比:
UI 卡顿 = 一条车道
- 只有一条车道(主线程)
- 如果有一辆车开得很慢(耗时任务)
- 后面的车都超不过去
- 堵车 = UI 卡顿
后台线程 = 新建车道
- 新建一条车道(后台线程)
- 慢车走新车道
- 快车走旧车道
- 互不干扰,畅通无阻
代码示例:后台任务处理
public class BackgroundTaskExample {
public static void main(String[] args) {
Display display = new Display();
Shell shell = new Shell(display);
shell.setText("后台任务处理示例");
shell.setLayout(new GridLayout(1, false));
// 进度条
ProgressBar progressBar = new ProgressBar(shell, SWT.SMOOTH);
progressBar.setMinimum(0);
progressBar.setMaximum(100);
GridData progressData = new GridData();
progressData.horizontalAlignment = GridData.FILL;
progressData.grabExcessHorizontalSpace = true;
progressBar.setLayoutData(progressData);
// 状态标签
Label statusLabel = new Label(shell, SWT.CENTER);
statusLabel.setText("状态:就绪");
// 按钮面板
Composite buttonPanel = new Composite(shell, SWT.NONE);
buttonPanel.setLayout(new FillLayout(SWT.HORIZONTAL));
// 错误按钮
Button wrongButton = new Button(buttonPanel, SWT.PUSH);
wrongButton.setText("错误:主线程执行");
wrongButton.addListener(SWT.Selection, event -> {
wrongButton.setEnabled(false);
// 错误!在主线程执行耗时任务,UI 卡顿
try {
for (int i = 0; i <= 100; i++) {
Thread.sleep(50);
progressBar.setSelection(i);
statusLabel.setText("进度:" + i + "%");
}
wrongButton.setEnabled(true);
statusLabel.setText("任务完成!");
} catch (InterruptedException e) {
e.printStackTrace();
}
});
// 正确按钮
Button correctButton = new Button(buttonPanel, SWT.PUSH);
correctButton.setText("正确:后台线程执行");
correctButton.addListener(SWT.Selection, event -> {
correctButton.setEnabled(false);
// 正确!在后台线程执行耗时任务,UI 流畅
new Thread(() -> {
try {
for (int i = 0; i <= 100; i++) {
final int progress = i;
// 主线程:更新 UI
display.asyncExec(() -> {
progressBar.setSelection(progress);
statusLabel.setText("进度:" + progress + "%");
});
Thread.sleep(50);
}
// 任务完成
display.asyncExec(() -> {
correctButton.setEnabled(true);
statusLabel.setText("任务完成!");
});
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
});
shell.setBounds(100, 100, 600, 200);
shell.open();
while (!shell.isDisposed()) {
if (!display.readAndDispatch()) {
display.sleep();
}
}
display.dispose();
}
}
10.3 从后台线程更新 UI
Display.asyncExec()
// asyncExec:异步执行(立即返回)
display.asyncExec(() -> {
// 在主线程执行
button.setText("更新");
});
System.out.println("asyncExec 立即返回");
费曼解释:asyncExec 的工作原理
asyncExec = 发送快递
- 你(后台线程)把快递(任务)给快递员(asyncExec)
- 快递员把快递送到前台(主线程)
- 你不用等快递员回来,可以继续做其他事
- 快递员到了前台,把快递交给前台服务员(执行任务)
类比:
asyncExec = 发送邮件
- 你写好邮件(任务)
- 点击"发送"(asyncExec)
- 邮件系统把邮件送到收件人(主线程)
- 你不用等收件人收到邮件,可以继续做其他事
- 收件人收到邮件,阅读邮件(执行任务)
Display.syncExec()
// syncExec:同步执行(等待完成)
display.syncExec(() -> {
// 在主线程执行
button.setText("更新");
});
System.out.println("syncExec 等待完成后才打印");
费曼解释:syncExec 的工作原理
syncExec = 送快递并等待回复
- 你(后台线程)把快递(任务)给快递员(syncExec)
- 快递员把快递送到前台(主线程)
- 快递员等待前台服务员处理完(执行任务)
- 快递员把回复给你
- 你收到回复后,继续做其他事
类比:
syncExec = 打电话
- 你(后台线程)给前台(主线程)打电话(syncExec)
- 前台接听电话,执行任务
- 你等待前台完成任务
- 前台完成任务,挂断电话
- 你收到回复,继续做其他事
两者的区别与选择
// asyncExec:异步执行,不阻塞后台线程
display.asyncExec(() -> {
button.setText("asyncExec");
});
System.out.println("asyncExec 不阻塞");
// syncExec:同步执行,阻塞后台线程
display.syncExec(() -> {
button.setText("syncExec");
});
System.out.println("syncExec 阻塞完成后才打印");
费曼解释:如何选择?
asyncExec:选择场景
- 只需要更新 UI,不需要返回值
- 不关心更新何时完成
- 推荐:大多数情况
syncExec:选择场景
- 需要等待 UI 更新完成
- 需要获取 UI 的返回值
- 谨慎使用:可能阻塞后台线程
类比:
asyncExec = 发送短信
- 发送短信
- 不等回复
- 继续做其他事
syncExec = 打电话
- 打电话
- 等对方接听并回复
- 收到回复后,继续做其他事
代码示例:asyncExec vs syncExec
public class AsyncSyncExample {
public static void main(String[] args) {
Display display = new Display();
Shell shell = new Shell(display);
shell.setText("asyncExec vs syncExec");
shell.setLayout(new GridLayout(1, false));
// 文本框:显示日志
Text logText = new Text(shell, SWT.BORDER | SWT.MULTI | SWT.V_SCROLL);
GridData logData = new GridData();
logData.horizontalAlignment = GridData.FILL;
logData.verticalAlignment = GridData.FILL;
logData.grabExcessHorizontalSpace = true;
logData.grabExcessVerticalSpace = true;
logData.heightHint = 200;
logText.setLayoutData(logData);
// 添加日志的辅助方法
shell.getDisplay().asyncExec(() -> {
logText.append("主线程:" + Thread.currentThread().getName() + "\n");
});
// 按钮:asyncExec
Button asyncButton = new Button(shell, SWT.PUSH);
asyncButton.setText("asyncExec");
asyncButton.addListener(SWT.Selection, event -> {
new Thread(() -> {
logText.append("后台线程:" + Thread.currentThread().getName() + "\n");
long startTime = System.currentTimeMillis();
display.asyncExec(() -> {
logText.append("asyncExec:主线程:" + Thread.currentThread().getName() + "\n");
});
long endTime = System.currentTimeMillis();
logText.append("asyncExec 耗时:" + (endTime - startTime) + "ms\n");
logText.append("asyncExec 立即返回,不阻塞\n");
}).start();
});
// 按钮:syncExec
Button syncButton = new Button(shell, SWT.PUSH);
syncButton.setText("syncExec");
syncButton.addListener(SWT.Selection, event -> {
new Thread(() -> {
logText.append("后台线程:" + Thread.currentThread().getName() + "\n");
long startTime = System.currentTimeMillis();
display.syncExec(() -> {
logText.append("syncExec:主线程:" + Thread.currentThread().getName() + "\n");
});
long endTime = System.currentTimeMillis();
logText.append("syncExec 耗时:" + (endTime - startTime) + "ms\n");
logText.append("syncExec 等待完成后才返回\n");
}).start();
});
shell.setBounds(100, 100, 600, 400);
shell.open();
while (!shell.isDisposed()) {
if (!display.readAndDispatch()) {
display.sleep();
}
}
display.dispose();
}
}
10.4 进度对话框(ProgressMonitorDialog)
后台任务的进度反馈
// 使用 ProgressMonitorDialog 显示进度
Button button = new Button(shell, SWT.PUSH);
button.setText("执行任务");
button.addListener(SWT.Selection, event -> {
ProgressMonitorDialog dialog = new ProgressMonitorDialog(shell);
try {
dialog.run(true, true, monitor -> {
monitor.beginTask("执行任务中...", 100);
for (int i = 0; i <= 100; i++) {
if (monitor.isCanceled()) {
throw new OperationCanceledException();
}
monitor.subTask("进度:" + i + "%");
monitor.worked(1);
Thread.sleep(50); // 模拟耗时操作
}
monitor.done();
});
} catch (InvocationTargetException | InterruptedException e) {
e.printStackTrace();
} catch (OperationCanceledException e) {
// 用户取消了任务
System.out.println("任务被取消");
}
});
费曼解释:ProgressMonitorDialog 的原理
ProgressMonitorDialog = 下载窗口
- 显示进度条
- 显示当前任务
- 用户可以取消
- 自动处理线程问题
类比:
ProgressMonitorDialog = 复印机
- 显示进度:第 10/100 页
- 显示当前任务:正在复印
- 用户可以取消:点击"停止"
- 自动处理:你不用管复印机内部怎么工作
取消操作的处理
// 处理取消操作
try {
dialog.run(true, true, monitor -> {
monitor.beginTask("执行任务中...", 100);
for (int i = 0; i <= 100; i++) {
if (monitor.isCanceled()) {
throw new OperationCanceledException();
}
monitor.subTask("进度:" + i + "%");
monitor.worked(1);
Thread.sleep(50);
}
monitor.done();
});
} catch (OperationCanceledException e) {
System.out.println("用户取消了任务");
}
费曼解释:取消操作的原理
取消操作 = 拔掉插头
- 用户点击"取消"(拔插头)
- monitor.isCanceled() 返回 true(检测到没电)
- 抛出 OperationCanceledException(停止任务)
- 对话框关闭(任务结束)
类比:
取消操作 = 拔掉电脑插头
- 你点击"取消"(拔插头)
- 程序检测到断电(isCanceled)
- 程序停止(抛出异常)
- 对话框关闭(任务结束)
代码示例:ProgressMonitorDialog
public class ProgressMonitorDialogExample {
public static void main(String[] args) {
Display display = new Display();
Shell shell = new Shell(display);
shell.setText("ProgressMonitorDialog 示例");
shell.setLayout(new GridLayout(1, false));
// 按钮:执行任务
Button runButton = new Button(shell, SWT.PUSH);
runButton.setText("执行任务");
runButton.addListener(SWT.Selection, event -> {
ProgressMonitorDialog dialog = new ProgressMonitorDialog(shell);
try {
dialog.run(true, true, monitor -> {
monitor.beginTask("执行任务中...", 100);
for (int i = 0; i <= 100; i++) {
if (monitor.isCanceled()) {
throw new OperationCanceledException();
}
monitor.subTask("进度:" + i + "%");
monitor.worked(1);
Thread.sleep(50); // 模拟耗时操作
}
monitor.done();
// 任务完成
display.asyncExec(() -> {
MessageBox messageBox = new MessageBox(shell, SWT.ICON_INFORMATION | SWT.OK);
messageBox.setText("提示");
messageBox.setMessage("任务完成!");
messageBox.open();
});
});
} catch (InvocationTargetException | InterruptedException e) {
e.printStackTrace();
} catch (OperationCanceledException e) {
// 用户取消了任务
display.asyncExec(() -> {
MessageBox messageBox = new MessageBox(shell, SWT.ICON_INFORMATION | SWT.OK);
messageBox.setText("提示");
messageBox.setMessage("任务被取消!");
messageBox.open();
});
}
});
shell.setBounds(100, 100, 400, 100);
shell.open();
while (!shell.isDisposed()) {
if (!display.readAndDispatch()) {
display.sleep();
}
}
display.dispose();
}
}
10.5 实战案例:文件复制进度条
public class FileCopyExample {
public static void main(String[] args) {
Display display = new Display();
Shell shell = new Shell(display);
shell.setText("文件复制进度条");
shell.setLayout(new GridLayout(1, false));
// 源文件
Label sourceLabel = new Label(shell, SWT.NONE);
sourceLabel.setText("源文件:");
Text sourceText = new Text(shell, SWT.BORDER);
sourceText.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false));
Button sourceButton = new Button(shell, SWT.PUSH);
sourceButton.setText("选择源文件");
sourceButton.addListener(SWT.Selection, event -> {
FileDialog fileDialog = new FileDialog(shell, SWT.OPEN);
fileDialog.setText("选择源文件");
String file = fileDialog.open();
if (file != null) {
sourceText.setText(file);
}
});
// 目标文件
Label targetLabel = new Label(shell, SWT.NONE);
targetLabel.setText("目标文件:");
Text targetText = new Text(shell, SWT.BORDER);
targetText.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false));
Button targetButton = new Button(shell, SWT.PUSH);
targetButton.setText("选择目标文件");
targetButton.addListener(SWT.Selection, event -> {
FileDialog fileDialog = new FileDialog(shell, SWT.SAVE);
fileDialog.setText("选择目标文件");
String file = fileDialog.open();
if (file != null) {
targetText.setText(file);
}
});
// 进度条
ProgressBar progressBar = new ProgressBar(shell, SWT.SMOOTH);
progressBar.setMinimum(0);
progressBar.setMaximum(100);
progressBar.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false));
// 状态标签
Label statusLabel = new Label(shell, SWT.CENTER);
statusLabel.setText("状态:就绪");
// 复制按钮
Button copyButton = new Button(shell, SWT.PUSH);
copyButton.setText("开始复制");
copyButton.addListener(SWT.Selection, event -> {
String sourceFile = sourceText.getText();
String targetFile = targetText.getText();
if (sourceFile.isEmpty() || targetFile.isEmpty()) {
MessageBox messageBox = new MessageBox(shell, SWT.ICON_WARNING | SWT.OK);
messageBox.setText("警告");
messageBox.setMessage("请选择源文件和目标文件!");
messageBox.open();
return;
}
copyButton.setEnabled(false);
new Thread(() -> {
try {
// 复制文件
copyFileWithProgress(sourceFile, targetFile, progressBar, statusLabel);
// 复制完成
display.asyncExec(() -> {
copyButton.setEnabled(true);
statusLabel.setText("复制完成!");
MessageBox messageBox = new MessageBox(shell, SWT.ICON_INFORMATION | SWT.OK);
messageBox.setText("提示");
messageBox.setMessage("文件复制完成!");
messageBox.open();
});
} catch (Exception e) {
e.printStackTrace();
display.asyncExec(() -> {
copyButton.setEnabled(true);
statusLabel.setText("复制失败:" + e.getMessage());
});
}
}).start();
});
shell.setBounds(100, 100, 500, 300);
shell.open();
while (!shell.isDisposed()) {
if (!display.readAndDispatch()) {
display.sleep();
}
}
display.dispose();
}
private static void copyFileWithProgress(String source, String target,
ProgressBar progressBar, Label statusLabel) throws IOException {
Path sourcePath = Paths.get(source);
Path targetPath = Paths.get(target);
long fileSize = Files.size(sourcePath);
long copiedBytes = 0;
try (InputStream in = Files.newInputStream(sourcePath);
OutputStream out = Files.newOutputStream(targetPath)) {
byte[] buffer = new byte[8192];
int bytesRead;
while ((bytesRead = in.read(buffer)) != -1) {
out.write(buffer, 0, bytesRead);
copiedBytes += bytesRead;
// 更新进度
final int progress = (int)(copiedBytes * 100 / fileSize);
display.asyncExec(() -> {
progressBar.setSelection(progress);
statusLabel.setText("复制进度:" + progress + "%");
});
}
}
}
}
费曼解释:文件复制的进度反馈
文件复制 = 搬家
- 源文件 = 旧家
- 目标文件 = 新家
- 文件内容 = 家具
- 复制 = 搬运家具
- 进度条 = 搬了多少
- 状态标签 = 搬到哪了
类比:
文件复制 = 餐厅上菜
- 源文件 = 厨房
- 目标文件 = 餐桌
- 文件内容 = 菜品
- 复制 = 上菜
- 进度条 = 上了多少
- 状态标签 = 上了什么菜
10.6 本章小结
线程与 SWT 总结
| 概念 | 作用 | 类比 |
|---|---|---|
| 主线程 | 操作 UI | 舞台演员 |
| 后台线程 | 执行耗时任务 | 后台工作人员 |
| asyncExec | 异步更新 UI | 发送快递 |
| syncExec | 同步更新 UI | 打电话 |
| ProgressMonitorDialog | 显示进度 | 下载窗口 |
费曼测试:你能解释清楚吗?
- 为什么不能跨线程操作 UI?
- UI 线程有"权限",后台线程没有,强行操作会抛出异常
- asyncExec 和 syncExec 的区别?
- asyncExec:异步执行,立即返回 - syncExec:同步执行,等待完成
- 如何避免 UI 卡顿?
- 在后台线程执行耗时任务,主线程只负责更新 UI
- ProgressMonitorDialog 的用途?
- 显示后台任务的进度,支持取消操作
下一章预告
现在你已经掌握了线程与 SWT,
UI 始终流畅响应,
但还不知道如何正确管理 SWT 资源。
下一章,我们将学习资源管理,
避免内存泄漏,
让应用长期稳定运行。
练习题:
- 创建一个后台任务,计算斐波那契数列,通过 asyncExec 更新 UI。
- 使用 ProgressMonitorDialog 显示后台任务的进度,支持取消操作。
- 创建一个文件复制程序,显示复制进度。
(提示:斐波那契数列计算放在后台线程,通过 asyncExec 更新 UI)