
JSch 的 SftpProgressMonitor.count() 虽在 EDT 中执行,但因 EDT 被阻塞而无法触发重绘,导致进度界面“冻结”;根本原因在于 Swing 的 repaint 机制依赖 EDT 空闲,而同步 SFTP 操作长期占用该线程。
jsch 的 `sftpprogressmonitor.count()` 虽在 edt 中执行,但因 edt 被阻塞而无法触发重绘,导致进度界面“冻结”;根本原因在于 swing 的 repaint 机制依赖 edt 空闲,而同步 sftp 操作长期占用该线程。
Swing 是单线程 GUI 框架,所有 UI 更新(如 setValueAt()、repaint())必须在事件分派线程(EDT)中执行,但仅“在 EDT 上执行”不等于“UI 实时响应”。关键在于:Swing 的重绘并非立即生效,而是通过 RepaintManager 将重绘请求加入 EDT 的事件队列,等待当前 EDT 任务完成后才批量处理。当 SftpProgressMonitor.count() 被 JSch 在同步 SFTP 传输期间高频回调时,整个调用链(包括 count() 本身)均运行在 EDT 上——这意味着 EDT 正在持续执行传输逻辑,始终处于“繁忙”状态,无暇处理挂起的 repaint 请求,从而造成 UI 卡顿假象。
以下是一个典型错误示例(即问题中的代码)及其修正方案:
// ❌ 错误:在 EDT 中执行阻塞式 SFTP 操作(如 channel.put() 同步调用)
public void startTransfer() {
// 此方法若在 EDT 中被调用(如按钮点击事件),将彻底阻塞 UI
sftpChannel.put(localPath, remotePath, new MySftpProgressMonitor(), ChannelSftp.OVERWRITE);
}
✅ 正确做法:将 SFTP 操作移出 EDT,使用 SwingWorker 实现后台执行与安全 UI 回调:
class SftpTransferTask extends SwingWorker<void integer> {
private final String localPath;
private final String remotePath;
private final JTable table;
private final DefaultTableModel model;
public SftpTransferTask(String localPath, String remotePath, JTable table, DefaultTableModel model) {
this.localPath = localPath;
this.remotePath = remotePath;
this.table = table;
this.model = model;
}
@Override
protected Void doInBackground() throws Exception {
// ✅ 在后台线程执行耗时操作
sftpChannel.put(localPath, remotePath, new SftpProgressMonitor() {
@Override
public boolean count(long bytes) {
// ✅ 计算进度并发布到 EDT(线程安全)
int percentage = computePercentage(bytes);
publish(percentage); // 触发 process() 回调
return true;
}
@Override
public void init(int op, String src, String dest, long maxBytes) {
// 可在此处 publish 初始化信息,或直接在 doInBackground 开头处理
SwingUtilities.invokeLater(() -> {
model.addRow(new Object[]{src, 0});
});
}
@Override
public void end() {}
}, ChannelSftp.OVERWRITE);
return null;
}
@Override
protected void process(List<integer> chunks) {
// ✅ 在 EDT 中安全更新 UI(自动保证线程安全)
for (int pct : chunks) {
table.setValueAt(pct, 0, 1); // 行 0,列 1 显示进度
}
}
@Override
protected void done() {
try {
get(); // 获取结果,捕获异常
table.setValueAt(100, 0, 1);
} catch (Exception e) {
JOptionPane.showMessageDialog(table, "传输失败: " + e.getMessage());
}
}
}
// 启动任务(在按钮点击事件等 EDT 上调用)
new SftpTransferTask("/local/file.zip", "/remote/file.zip", table, model).execute();</integer></void>
⚠️ 注意事项:
-
切勿在 EDT 中调用任何阻塞 I/O 方法(如
channel.put()、channel.get()),即使实现了SftpProgressMonitor; -
SwingWorker.publish()和process()是 Swing 提供的专用于跨线程安全更新 UI 的机制,比手动SwingUtilities.invokeLater()更简洁可控; - 若需支持取消操作,可重写
doInBackground()中的循环逻辑,并在isCancelled()为true时中断传输(JSch 本身不支持中断,需配合超时或自定义标志位); -
DefaultTableModel的addRow()等方法不是线程安全的,务必在SwingUtilities.invokeLater()或process()中调用。
总结:UI 冻结的本质是 EDT 饥饿而非线程切换错误。解决的核心原则是——让 EDT 始终保持空闲以响应事件和重绘,所有耗时工作交由后台线程完成,并通过受控机制(如 SwingWorker)桥接线程间通信。这是 Swing 应用开发的基石实践,适用于 JSch、数据库查询、文件解析等所有长耗时场景。











