本文目录导读:

这是一个关于 JEditorPane + HTMLEditorKit 在解析 HTML 时出现死锁的经典问题,这个 Bug 在 Java 6/7/8 中都比较常见,尤其是在多线程环境下加载或设置 HTML 内容时。
问题根源
死锁通常发生在以下场景:
Thread 1: 持有 A 锁,等待 B 锁
Thread 2: 持有 B 锁,等待 A 锁
在 HTMLEditorKit 的 HTML 解析器中,具体表现为:
- Swing 事件分发线程 (EDT):持有某个解析器相关的锁
- 后台加载线程:也试图获取同一个锁,但可能已经在另一个锁上阻塞
典型死锁场景
// 在非 EDT 线程中修改 HTML
new Thread(() -> {
// 可能触发死锁
editorPane.setText("<html>...</html>");
}).start();
或者在 EDT 中:
SwingUtilities.invokeLater(() -> {
// 如果这个操作触发了复杂的 HTML 解析...
editorPane.setText(complexHtmlContent);
});
解决方案
始终在 EDT 中操作 JEditorPane
SwingUtilities.invokeLater(() -> {
editorPane.setText(htmlContent);
});
使用 DocumentFilter 或自定义 Document
// 使用 Document 直接操作,避免 HTML 解析器的锁竞争
String html = "<html><body>Content</body></html>";
SwingUtilities.invokeLater(() -> {
try {
HTMLEditorKit kit = (HTMLEditorKit) editorPane.getEditorKit();
HTMLDocument doc = (HTMLDocument) kit.createDefaultDocument();
kit.read(new StringReader(html), doc, 0);
editorPane.setDocument(doc);
} catch (Exception e) {
e.printStackTrace();
}
});
使用 SwingWorker 处理复杂加载
class HtmlLoader extends SwingWorker<Void, Void> {
private final String url;
private final JEditorPane editorPane;
HtmlLoader(String url, JEditorPane editorPane) {
this.url = url;
this.editorPane = editorPane;
}
@Override
protected Void doInBackground() throws Exception {
// 在后台线程中下载内容
String html = downloadHtml(url);
// 在 EDT 中更新 UI
SwingUtilities.invokeLater(() -> {
editorPane.setText(html);
});
return null;
}
}
避免在非 EDT 线程中直接调用 setText
如果必须在后台线程中准备 HTML,使用 StringBuilder 构建完成后,再通过 invokeLater 设置:
// 后台线程
String html = buildHtmlContent();
SwingUtilities.invokeLater(() -> {
editorPane.setText(html);
});
考虑使用替代组件
如果死锁问题持续出现且严重影响应用,考虑使用:
- JavaFX WebView:对于复杂 HTML 渲染更稳定
- JTextPane +自定义渲染:对于简单 HTML
- 第三方库:如
Flying Saucer(XHTML renderer)
调试死锁
如果怀疑发生死锁,可以通过 jstack 或 Thread Dump 来分析:
# 获取 Java 进程的线程转储 jstack <pid> > thread_dump.txt
在 dump 中查找类似的阻塞情况:
"Thread-1" #12 prio=5 os_prio=0 tid=0x00007f...
java.lang.Thread.State: BLOCKED
at javax.swing.text.html.HTMLEditorKit.parse
- waiting to lock <0x000000076b...>
- locked <0x000000076c...>
"AWT-EventQueue-0" #11 prio=6 os_prio=0 tid=0x00007f...
java.lang.Thread.State: BLOCKED
at javax.swing.text.html.HTMLEditorKit.parse
- waiting to lock <0x000000076c...>
- locked <0x000000076b...>
预防措施
- 只使用 EDT 线程更新任何 Swing 组件
- 避免在事件监听器中执行耗时操作
- 使用
SwingUtilities.invokeLater包装所有 UI 更新 - 考虑线程安全的文档操作,特别是涉及 HTML 解析时
完整示例
import javax.swing.*;
import javax.swing.text.html.*;
import java.io.*;
public class SafeHtmlEditor extends JPanel {
private JEditorPane editorPane;
public SafeHtmlEditor() {
editorPane = new JEditorPane();
editorPane.setEditorKit(new HTMLEditorKit());
editorPane.setEditable(false);
add(new JScrollPane(editorPane));
}
public void safeSetHtml(String html) {
// 始终在 EDT 中操作
if (SwingUtilities.isEventDispatchThread()) {
doSetHtml(html);
} else {
SwingUtilities.invokeLater(() -> doSetHtml(html));
}
}
private void doSetHtml(String html) {
try {
HTMLEditorKit kit = (HTMLEditorKit) editorPane.getEditorKit();
HTMLDocument doc = new HTMLDocument();
kit.read(new StringReader(html), doc, 0);
editorPane.setDocument(doc);
} catch (Exception ex) {
ex.printStackTrace();
}
}
}
遵循这些建议,应该能有效避免 JEditorPane + HTMLEditorKit 的死锁问题,如果问题仍然存在,可能需要考虑更换渲染技术或使用更现代的 UI 框架。