我有一个简单的控制台应用程序,它在几个线程中运行计算(其中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的新手,所以我无法理解我在这里做错了什么?
提前致谢。
答案 0 :(得分:5)
你不想直接从另一个线程操纵Swing对象,你想要post manipulations to it's event queue。
答案 1 :(得分: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 :(得分:2)
Max,你没有提到,你是interrupt
线程。但你肯定做到了。所以你的问题实际上包含两个不同的问题。
append
有时会抛出InterruptedException
我刚陷入同样的境地,不知道如何处理它。当我中断线程时,Document.insertString
无法抛出这种错误。
其他人对于将所有内容放入EDT主题并不完全正确。 JTextArea.append
方法是线程安全的,因此无需包装。您调用的唯一方法(工作线程)是setCaretPosition
。那你为什么接受invokeLater
答案呢?可能是因为将文档访问放在一个线程中会删除所有锁定问题。请参阅AbstractDocument.writeLock
open jdk code,其中解释了这一点Error
。
因此看起来在EDT线程中放置Document
写入是非常必要的,但只有在想要中断线程时才会这样。作为非常不友好的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
}
}
});
}
}
答案 3 :(得分:0)
我知道这是一篇老文章,但是如果对JTextArea的更新频率很高,则使用setCaretPosition()方法将视口自动滚动到底部会导致死锁问题。仅在EDT上进行更新并不能避免该问题或提供任何改进。
避免出现此问题的方法:
以下是我创建的用于将文本捕获到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;
}
}
如果滚动窗格不可用,则使用脱字符号方法,可以在其中指定节流以最大程度地减少摆动代码内死锁的风险。