JEditorPaneHTMLEditorKitParserMethodConcurrentModificationException并发修改

wen java案例 2

本文目录导读:

JEditorPaneHTMLEditorKitParserMethodConcurrentModificationException并发修改

  1. 核心原因
  2. 为什么 JEditorPane 特别容易中招?
  3. 解决方案(优先级从高到低)
  4. 调试技巧

这是一个经典且棘手的 Java Swing 问题,JEditorPane + HTMLEditorKit + Parser 在并发环境下非常容易出现 ConcurrentModificationException

核心原因

HTMLEditorKit 内部使用的 Parser(如 javax.swing.text.html.parser.ParserDelegator)在解析 HTML 时,会回调 HTMLEditorKit.ParserCallback 的各种方法(如 handleText, handleStartTag, handleEndTag 等)。

问题发生的典型场景是:

  1. 主线程(EDT,也就是事件调度线程) 正在使用 JEditorPane.setText() 或直接操作文档模型(Document)来显示 HTML。
  2. 在这个过程中,Parser 正在遍历内部数据结构(如标签栈、属性列表、字符数组)。
  3. 另一个线程(并非 EDT,而是 EDT 之外的线程)同时尝试:
    • 调用 setText() 设置新的 HTML。
    • 直接修改 Document(例如插入、删除字符)。
    • 或者 Parser 内部的回调逻辑中,错误地触发了对同一个 Document 的修改。

由于 Java 集合(ArrayListHashMap 等)在迭代(遍历)时被结构性修改,ConcurrentModificationException 就会抛出。

为什么 JEditorPane 特别容易中招?

  • 非线程安全: JEditorPane 和其内部的 Document 类(如 HTMLDocument)不是为多线程并发访问设计的,所有的 Swing 组件操作都应该在 事件调度线程(EDT) 上执行。
  • 解析器的内部迭代: Parser 在解析时会创建多个内部迭代器来扫描 HTML 标签和属性,如果用户在 ParserCallback 方法实现里(handleText 方法中)调用 setText()append() 来修改文档,就会在解析器迭代过程中修改文档结构,触发异常。

解决方案(优先级从高到低)

确保所有修改都在 EDT 上执行(最推荐)

这是 Swing 的黄金法则,任何对 JEditorPane 及其 Document 的修改,都必须通过 SwingUtilities.invokeLaterSwingUtilities.invokeAndWait 提交到 EDT。

// 错误示例:在后台线程中直接修改
new Thread(() -> {
    // 模拟耗时计算
    String newHtml = "<html><body>新内容</body></html>";
    // 这里会抛出 ConcurrentModificationException
    editorPane.setText(newHtml);
}).start();
// 正确示例:通过 invokeLater 提交到 EDT
new Thread(() -> {
    // 模拟耗时计算
    String newHtml = "<html><body>新内容</body></html>";
    // 将修改操作提交到事件调度线程
    SwingUtilities.invokeLater(() -> {
        editorPane.setText(newHtml);
    });
}).start();

避免在回调方法中修改文档

如果你重写了 HTMLEditorKit.ParserCallback 的方法(如 handleText),一定要避免在这些方法内部直接调用 setText()insertString()remove() 等会修改文档结构的方法,因为此时解析器正在遍历内部结构。

// 错误示例:在回调中修改文档
HTMLEditorKit.ParserCallback callback = new HTMLEditorKit.ParserCallback() {
    @Override
    public void handleText(char[] data, int pos) {
        // 这里会抛出 ConcurrentModificationException!!!
        ((HTMLDocument) editorPane.getDocument()).setText("修改一下");
    }
};

正确做法: 使用 SwingUtilities.invokeLater 将后续操作延迟到 EDT 的空闲时间执行。

@Override
public void handleText(char[] data, int pos) {
    String text = new String(data);
    if (text.contains("特定内容")) {
        // 不要直接修改,而是安排一个延迟任务
        SwingUtilities.invokeLater(() -> {
            editorPane.setText("<html><body>修改后的内容</body></html>");
        });
    }
}

使用 DocumentFilterDocumentListener 的注意事项

如果使用 DocumentFilterDocumentListener 来监控文档变化,请确保在它们的回调方法中 不要再次修改同一个文档对象,否则会陷入无限递归或并发修改异常,如果需要修改,使用 SwingUtilities.invokeLater

同步锁(不太推荐,但可应急)

如果你必须从多个线程间接访问(一个后台线程需要触发解析器的运行),可以使用同步块,但要非常小心死锁。

// 使用一个锁对象
private final Object documentLock = new Object();
public void updateHtmlFromBackgroundThread(String newHtml) {
    synchronized (documentLock) {
        SwingUtilities.invokeLater(() -> {
            editorPane.setText(newHtml);
        });
    }
}

注意: 这种方法依然强烈推荐只使用 invokeLater,因为 Swing 组件默认就是单线程模型,使用锁会增加复杂度和出错风险。

关闭或替换默认解析器(高级用法)

如果默认的 ParserDelegator 在你的特定场景下总是出问题,你可以在创建 EditorKit 时禁用解析器或使用自定义的更安全的解析器,但这个方法比较极端,通常不需要。

调试技巧

  1. 检查调用栈: 当异常发生时,查看调用栈,你是从 EDT 还是从后台线程调用的?异常是在 setText() 中触发的,还是在 ParserCallback 的回调中触发的?
  2. 使用 SwingUtilities.isEventDispatchThread() 在关键修改操作前打印日志,确认当前线程是否是 EDT。
    if (SwingUtilities.isEventDispatchThread()) {
        System.out.println("在 EDT 上执行,安全");
    } else {
        System.out.println("危险!不在 EDT 上!");
    }
  3. 简化重现: 创建一个最小的可运行示例,只包含 JEditorPane、一个后台线程和简单的 setText 调用,看是否复现,如果能复现,99% 是线程问题。
场景 解决方案
后台线程修改 JEditorPane 必须使用 SwingUtilities.invokeLater()
ParserCallback 中修改文档 不允许,改用 invokeLater 延迟执行
多线程访问 强制所有操作在 EDT 上执行,这是 Swing 唯一正确的方式
复杂文档操作 使用 DocumentFilter 并确保不在回调中修改自身

最直接、最有效的解决方法是:只要修改 JEditorPane 或它的 Document,就把代码放到 SwingUtilities.invokeLater(() -> { ... }) 里面,所有其他方法都是锦上添花。

抱歉,评论功能暂时关闭!