Swing JTextArea多线程问题 - InterruptedException

6
我有一个简单的控制台应用程序,可以在多个线程(10-20个)中运行计算。现在我正在尝试创建一个简单的GUI,允许我选择要处理的文件并打印所有线程的日志。
因此,我创建了一个带有JTextArea的swing GUI,用于记录日志信息的方法:
public synchronized void log(String text) {
    logArea.append(text);
    logArea.append("\n");

    if (logArea.getDocument().getLength() > 50000) {
        try {
            logArea.getDocument().remove(0,5000);
        } catch (BadLocationException e) {
            log.error("Can't clean log", e);
        }
    }

    logArea.setCaretPosition(logArea.getDocument().getLength());
}

然而,setCaretPosition方法有时会在等待某个锁时发生死锁,append有时会抛出InterruptedException异常。

Exception in thread "Thread-401" java.lang.Error: Interrupted attempt to aquire write lock
at javax.swing.text.AbstractDocument.writeLock(AbstractDocument.java:1334)
at javax.swing.text.AbstractDocument.insertString(AbstractDocument.java:687)
at javax.swing.text.PlainDocument.insertString(PlainDocument.java:114)
at javax.swing.JTextArea.append(JTextArea.java:470)
at lt.quarko.aquila.scripts.ui.ScriptSessionFrame.log(ScriptSessionFrame.java:215)

我是Swing的新手,所以不明白我在这里做错了什么?

谢谢提前。

4个回答

5

这仍然无法解决处理高频更新时的死锁问题。请参见下面的解决方案。 - ewh

3

您不应该从其他线程更新UI组件,应该使用EventDispatchThread。以下是解决方案:

 public synchronized void log(String text) {
        Runnable  runnable = new Runnable() {
            public void run(){
                logArea.append(text);
                logArea.append("\n");
                if (logArea.getDocument().getLength() > 50000) {
                    try {
                        logArea.getDocument().remove(0, 5000);
                    } catch (BadLocationException e) {
                        log.error("Can't clean log", e);
                    }
                }
                logArea.setCaretPosition(logArea.getDocument().getLength());
            }
        }
        SwingUtilities.invokeLater(runnable);

    }

2

Max,你没有提到你中断了线程。但是你肯定中断了。所以你的问题实际上包含了两个单独的问题。

append 有时会抛出 InterruptedException

我也遇到了同样的情况,不知道该如何处理。当我中断线程时,Document.insertString 失败并抛出此类错误。

其他人关于将所有内容放在 EDT 线程中并不完全正确。JTextArea.append 方法是线程安全的,因此不需要包装。您调用的唯一方法(在工作线程中)不应该是 setCaretPosition。那么为什么您接受了 invokeLater 的答案?可能是因为将文档访问放在一个线程中消除了所有锁定问题。请参见 AbstractDocument.writeLock open jdk 代码,解释了一下这个 Error

因此,看起来将 Document 写入 EDT 线程中确实是必要的,但只有当您想中断线程时才需要。而作为对相当不友好的 AbstractDocument 行为的解决方法,在这��情况下会抛出一个 Error

我提出了以下对于 Document Error 的解决方法。它不太干净,因为线程在设置了 bInterrupted 标志后可能会被不幸地中断。但是可以通过以受控、同步的方式执行 Thread.interrupt() 来避免这种情况。

// test the flag and clear it (interrupted() method does clear it)
boolean bInterrupted = Thread.interrupted();
m_doc.insertString(m_doc.getLength(), s, null);
// restore the original interrupted state
if (bInterrupted)
  Thread.currentThread().interrupt();

setCaretPosition方法有时会在等待某些锁时死锁。

这是我的光标更新解决方案。我本可以直接使用invokeLater,但我想避免不必要的调用,所以我添加了一个额外的标志:

/** <code>true</code> when gui update scheduled. This flag is to avoid
  * multiple overlapping updates, not to call
  * <code>invokeLater</code> too frequently.
 */
private volatile boolean m_bUpdScheduled;

/** Updates output window so that the last line be visible */
protected void update()
{
  if (!m_bUpdScheduled) {
    m_bUpdScheduled = true;
    EventQueue.invokeLater(new Runnable() {
        public void run() {
          m_bUpdScheduled = false;
          try {
            m_ebOut.setCaretPosition(m_doc.getLength());
          }
          catch (IllegalArgumentException iae) {
            // doc not in sync with text field - too bad
          }
        }
    });
  }
}

1
JTextArea的append方法是线程安全的,这是一个重复的谬论,即使Swing开发者们也相信了 :-) 这是不正确的 - 参见最新的jdk 7 API文档 - 而且从来没有过(尽管有很多争论和激烈的讨论)。 - kleopatra
@kleopatra,感谢您指出这一点。然而,在jdk 6和7中,AbstractDocument.insertString是线程安全的。有关详细信息,请参见我的答案此处 - Jarekczek
请注意,即使进行高频更新,仍然可能出现死锁问题。请查看我下面的解决方案,了解如何避免或减轻死锁问题。 - ewh

0

我知道这是一个旧帖子,但使用setCaretPosition()方法自动滚动视口到底部可能会导致死锁问题,如果JTextArea的更新频率非常高。仅在EDT上进行更新并没有避免问题或提供任何改进。

避免问题的方法:

  • 不要尝试自动滚动,并将插入符号更新策略设置为NEVER_UPDATE。可能不是理想的解决方案,但在我们的主要应用程序中,我们有一个“滚动锁定”功能,因此可以在捕获文本时查看输出。
  • 限制使用setCaretPosition():如果处理大量更新,请在X次更新后设置插入符号。我通过行跟踪我们的应用程序诊断,因此每次都可以推迟插入符号更新,直到捕获了X行。请注意,如果限制,则仍需要将插入符号更新策略设置为NEVER_UPDATE。
  • 如果您的JTextArea位于JScrollPane中,则直接使用垂直滚动条,并在每次更新文本区域后将其重新定位到最大位置。确保在重新定位滚动条时使用invokeLater(),以便考虑由于添加文本而发生的任何大小更改。
以下是我创建的一个类的代码片段,用于捕获文本到JTextComponent(或其子类)中,并检查滚动窗格方法是否生效:
  if (autoScroll && (scrollPane != null)) {
    final JScrollBar vertical = scrollPane.getVerticalScrollBar();
    if (vertical != null) {
      appendText(doc, txt);
      java.awt.EventQueue.invokeLater(new Runnable() {
        @Override public void run() {
          vertical.setValue(vertical.getMaximum());
        }
      });
      return;
    }
  }

如果滚动面板不可用,则使用插入符号方法,其中可以指定节流以最小化摆动代码内死锁的风险。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接