JEditorPaneHTMLEditorKitParserCallerRuns调用者运行

wen java案例 1

本文目录导读:

JEditorPaneHTMLEditorKitParserCallerRuns调用者运行

  1. 问题背景与核心机制
  2. 问题的典型触发路径
  3. 为什么 CallerRunsPolicy 是罪魁祸首?
  4. 诊断方法
  5. 解决方案

这是一个非常专业且具体的 Java Swing 问题,你提到的 JEditorPaneHTMLEditorKitParserCallerRuns 这几个关键词组合在一起,通常指向一个特定的并发问题:

当使用 HTMLEditorKit 解析 HTML 时,如果解析任务在事件调度线程 (EDT) 上执行,并且抛出了一个未捕获的异常,那么该异常可能会被线程池的拒绝执行处理器(通常是 ThreadPoolExecutor.CallerRunsPolicy)捕获,从而导致意想不到的 EDT 阻塞或死锁。

让我为你详细解析这个问题的背景、原因及解决方案。

问题背景与核心机制

  • JEditorPane & HTMLEditorKit:Swing 中用于显示和编辑 HTML 的组件。HTMLEditorKit 内部使用一个 HTML 解析器(如 javax.swing.text.html.parser.ParserDelegator)来解析 HTML 文档。
  • 异步解析:为了提高响应性,HTMLEditorKit 在初始化或设置文本时,经常不会在主线程(EDT)上同步解析整个 HTML,它会将解析任务提交到一个后台工作线程上执行,解析完成后,再通过 SwingUtilities.invokeLater() 将结果更新到 EDT。
  • CallerRunsPolicy:这是 java.util.concurrent.ThreadPoolExecutor 的一个拒绝策略,当线程池已满且工作队列已满时,如果提交了新任务,该策略会直接在提交任务的线程中运行被拒绝的任务
  • 问题爆发点:这个组合问题通常发生在 Java 6/7 以及一些特定的 Swing 版本中,尤其是在处理部分格式错误或特定的 HTML 内容时。

问题的典型触发路径

  1. 你调用 setText():你在 EDT 上调用 jEditorPane.setText("<html>...复杂的HTML...")
  2. HTMLEditorKit 启动解析HTMLEditorKit 内部发现需要进行 HTML 解析,它创建一个解析任务(一个 Runnable),并将其提交给一个内部的线程池(通常是单线程的,用于序列化解析任务)。
  3. 解析任务执行:这个 Runnable 在你的后台线程池中执行,开始解析 HTML。
  4. 解析抛出异常:由于 HTML 内容的不规范(未闭合的标签、非法的字符实体、或者触发了 Parser 的 bug),解析器抛出一个 RuntimeException(如 ArrayIndexOutOfBoundsException)。
  5. 未捕获的异常:这个异常没有被后台线程的 run() 方法捕获。
  6. 线程池的线程消亡:因为未捕获异常,当前的后台线程终止。
  7. 线程池创建新线程:线程池检测到线程终止,并尝试创建一个新的线程。
  8. 新任务提交失败:当你的 HTML 解析任务再次被提交时,线程池可能因为线程创建失败、或者线程池本身被 shutdown(例如在组件被丢弃时) 而无法接受新任务。
  9. 触发 CallerRunsPolicy:作为默认的拒绝策略,当线程池无法接收任务时,它调用 CallerRunsPolicy
  10. 灾难发生CallerRunsPolicy 直接在当前线程(即步骤 8 中调用 execute() 的线程)上运行解析任务,如果这个线程恰好是 EDT,那么解析工作就突然跑到了 EDT 上,解析 HTML 是一个阻塞且耗时的大任务,会导致 EDT 被长时间阻塞,界面冻结。

为什么 CallerRunsPolicy 是罪魁祸首?

  • CallerRunsPolicy 的设计初衷:是为了在系统过载时,通过让调用者(提交任务的线程)亲自执行任务来减缓任务提交速度,并保证任务不会丢失。
  • 在 Swing 中的致命伤:Swing 强烈要求所有对 UI 组件的修改和长时间任务都不能在 EDT 上执行,但 CallerRunsPolicy 恰恰做了相反的事:它把后台任务强行塞回到调用线程(很可能是 EDT)上运行,导致 EDT 被占用来做了解析工作,最终界面卡死。

诊断方法

要确认你是否遇到了这个问题,可以检查以下几点:

  1. 线程栈:当界面冻结时,使用 jstack 或 IDE 的调试器,找到名为 AWT-EventQueue-0 的 EDT 线程,检查其调用栈是否包含:
    • javax.swing.text.html.parser.ParserDelegator
    • javax.swing.text.html.HTMLEditorKit 的解析方法
    • java.util.concurrent.ThreadPoolExecutor$CallerRunsPolicy.rejectedExecution
  2. 日志:观察控制台是否有 java.util.concurrent.RejectedExecutionException 的异常输出(即使被 CallerRunsPolicy 处理了,这个异常也经常会被记录)。
  3. Java 版本:这个问题在 Java 6 和 Java 7 中更常见,Java 8 及之后,Oracle 对 HTMLEditorKit 的解析进行了优化,减少了对内部线程池的依赖,或者改进了异常处理,使得问题明显减少。

解决方案

根据你的具体情况,有以下几种解决方案:

升级 Java 版本(推荐)

最根本的解决方案,升级到 Java 8 或更高版本,Oracle 在 Java 8 中显著改进了 HTMLEditorKit 的并发处理,不再那么容易触发 CallerRunsPolicy

setText() 之前手动确保干净的上下文

如果你无法升级 Java,可以在调用 setText() 前后进行更稳健的操作:

public void safeSetText(final JEditorPane pane, final String text) {
    // 确保 EDT 上执行
    if (SwingUtilities.isEventDispatchThread()) {
        // 尝试在 EDT 上直接设置,但可能触发后台解析
        pane.setText(text);
    } else {
        SwingUtilities.invokeLater(() -> pane.setText(text));
    }
}

但这并不能完全避免后台解析,更好的做法是完全在后台线程中处理 HTML 解析,然后在 EDT 上只做最终的显示设置

public void setHTMLContent(JEditorPane pane, String htmlContent) {
    new Thread(() -> {
        try {
            // 1. 在后台线程创建 EditorKit 并手动解析
            HTMLEditorKit kit = (HTMLEditorKit) pane.getEditorKit();
            Document doc = kit.createDefaultDocument();
            // 关键:在后台线程中解析
            kit.read(new StringReader(htmlContent), doc, 0);
            // 2. 解析完成后,在 EDT 上设置文档
            Document finalDoc = doc;
            SwingUtilities.invokeLater(() -> {
                pane.setDocument(finalDoc);
            });
        } catch (Exception e) {
            e.printStackTrace(); // 异常在后台线程被处理,不影响 EDT
            SwingUtilities.invokeLater(() -> {
                pane.setText("解析失败"); // 安全地更新 UI
            });
        }
    }).start();
}

注意kit.read() 方法本身会触发解析,但因为它发生在你手动创建的后台线程中,所以任何解析异常都会被你的 try-catch 捕获,不会影响到 EDT。

禁用内部线程池(不推荐,但可尝试)

通过 System.setProperty("sun.swing.text.html.parser.parserBuffer", "true") 或类似属性?我查证后发现,禁用内部线程池没有标准的安全 API。不要尝试通过反射修改 HTMLEditorKit 的内部 Executor,这会导致不可预知的行为。

使用自定义的 ThreadPoolExecutor 和拒绝策略

如果必须沿用 HTMLEditorKit 的内部线程池机制(实际上你无法直接控制它),你可以尝试监控线程池的状态,但更实际的是,接受 CallerRunsPolicy 的风险,并通过方案二完全自己控制解析线程

  • 根本原因HTMLEditorKit 内部线程池的 CallerRunsPolicy 将解析任务抛回到 EDT 上执行,导致界面冻结。
  • 最容易的修复升级到 Java 8 或更新版本
  • 无法升级时的最佳实践不要依赖 setText() 的内部解析,而是手动在后台线程中创建 Document,然后安全地设置到 EDT 上。

你的问题描述得非常精准,如果你能提供具体的异常栈或 Java 版本,我可以给出更精确的分析。

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