JTextArea线程安全吗?

3
我有一些代码进行了一些初始化操作(包括创建一个JTextArea对象),启动三个单独的线程,然后这些线程尝试更新JTextArea(即对其进行append()),但根本不起作用。在JTextArea初始化期间,我将一些测试行打印到它上面,这很好用。发生了什么?我该怎么解决?此外,每个线程每次更新JTextArea时都会睡眠随机时间。

抱歉,我没有提供任何代码,它们都分散在几个文件中。

5个回答

4
尽管我相信API已经声明JTextArea#append(...)是线程安全的,但我听说过它存在问题,并建议仅在EDT上调用。这个经典的例子是使用SwingWorker并通过调用publish在process方法中附加到JTextArea。
对于我来说,如果没有代码,很难给您提出具体建议。不过,我确实要想知道您是否在代码中让EDT休眠了。
编辑:根据您的评论,请查看此教程:Swing中的并发
编辑2:根据Tim Perry的评论,线程安全性的丧失和其背后的原因已经发布在此Java bug中,与此行代码有关,该代码将文本添加到JTextArea的Document中:
doc.insertString(doc.getLength(), str, null);

这行代码分解成两行:

  1. int len = doc.getLength();
  2. doc.insertString(len, str, null);

问题在于,如果文档(即doc)在第1行和第2行之间发生更改,尤其是文档长度的更改,可能会出现问题。


+1 同意。我看到在之前的 J2SE 7 版本中,一些 JTextArea 方法的线程安全 条款 已经被删除了。http://download.oracle.com/javase/7/docs/api/javax/swing/JTextArea.html - trashgod
1
http://bugs.java.com/view_bug.do?bug_id=4765383 解释了为什么它不再被列为线程安全。 - Tim Perry
1
@TimPerry:谢谢你的更新。我已经将你提供的信息融入到我的答案中了。 - Hovercraft Full Of Eels

3
在Java 1.6中,JTextArea.append的文档说:
将给定的文本追加到文档的末尾。如果模型为null或字符串为null或为空,则不执行任何操作。
虽然大多数Swing方法都不是线程安全的,但此方法是线程安全的。请参见如何使用线程以获取更多信息。
在JDK7中,第二部分缺失:
将给定的文本追加到文档的末尾。如果模型为null或字符串为null或为空,则不执行任何操作。
如果查看Document接口(JTextArea可以使用用户提供的实例),即使实现是线程安全的,也没有办法以线程安全的方式追加文本。Swing线程有问题。我强烈建议在接近Swing组件时严格遵守AWT EDT。

我对这个问题的理解有些困难:即使实现是线程安全的,也没有办法以线程安全的方式追加文本,因此我将尝试详细说明一下。我猜你的意思是:在Document接口中没有原子的append操作,所以要进行追加必须实际执行两个后续调用:getLengthinsertString。无法保证这两个调用之间内容不会发生变化。 - Jarekczek

2
我相信有经验的人警告不要相信Document的线程安全性。然而,很难相信一个应用程序会如此轻易地利用这个问题,导致JTextArea根本没有显示任何内容。可能除了append之外还使用了其他方法,导致总体失效。我附上了一个测试应用程序,在Debian上运行Oracle jre 6(也在Win7上运行java 6 64位),没有发现问题。
在开发过程中,我不得不修复几个错误,包括:
  1. 未同步getLength()insertString()方法,导致插入位置错误甚至出现BadLocationException。其他线程在这两个指令之间修改文档。即使它们在同一行 :)
  2. 吞掉BadLocationException。我确信这是不可能发生的,但我错了。
在意识到上述问题,特别是需要为getLength()insertString()配对创建关键部分的需求后,很明显JTextArea会失败(请参见Tom Hawtin的答案)。我确实看到了它的失败,因为并非每个insertString都成功执行,导致结果文本比应该的要短。然而,在循环次数为10000时,这个问题并没有发生,只有在100000时才出现。查看jdk 7 JTextArea.append代码,它仅修改底层文档,似乎在外部同步JTextArea也可以解决这个问题。

在0处插入也很好用,不需要任何同步,尽管需要很长时间才能完成。

通常在这样的应用程序中,人们希望滚动到最后一行。嘿,这是awt。您不能在EDT之外设置CaretPosition。所以我不这样做。手动滚动。我解决滚动问题的建议在另一个答案中。
如果你们在你们的系统上看到这个应用程序有问题,请评论。我的结论是,Document.insertString是线程安全的,但要有效地使用它,需要同步。 在下面的代码中,PlainDocument被子类化以创建同步的append方法,该方法将getLengthinsertString封装到锁中。这个锁有受保护的访问权限,所以我不能在没有单独的类的情况下使用它。然而,外部同步也给出了正确的结果。
顺便说一句:对于这么多次编辑我很抱歉。最后,在学到更多知识后,我重构了这个答案。
代码:
import java.awt.*;
import java.util.concurrent.CountDownLatch;
import javax.swing.*;
import javax.swing.text.*;

class SafePlainDocument extends PlainDocument
{
  public void append(String s)
  {
    writeLock();
    try {
      insertString(getLength(), s,  null);
    }
    catch (BadLocationException e) {
      e.printStackTrace();
    }
    finally
    {
      writeUnlock();
    }
  }
}

public class StressJText
{
  public static CountDownLatch m_latch;
  public static SafePlainDocument m_doc;
  public static JTextArea m_ta;

  static class MyThread extends Thread
  {
    SafePlainDocument m_doc;
    JTextArea m_ta;

    public MyThread(SafePlainDocument doc)
    {
      m_doc = doc;
    }

    public void run() 
    {
      for (int i=1; i<=100000; i++) {
        String s = String.format("%19s %9d\n", getName(), i);
        m_doc.append(s);
      }
      StressJText.m_latch.countDown();
    }
  }

  public static void main(String sArgs[])
  {
    System.out.println("hello");
    final int cThreads = 5;
    m_latch = new CountDownLatch(cThreads);
    java.awt.EventQueue.invokeLater(new Runnable() {
        public void run() {
          JFrame frame = new JFrame();
          m_ta = new JTextArea();
          m_doc = new SafePlainDocument();
          m_ta.setDocument(m_doc);
          m_ta.setColumns(50);
          m_ta.setRows(20);
          JScrollPane scrollPane = new javax.swing.JScrollPane();
          scrollPane.setViewportView(m_ta);
          frame.add(scrollPane);
          frame.pack();
          frame.setVisible(true);

          for (int it=1; it<=cThreads; it++) {
            MyThread t = new MyThread(m_doc);
            t.start();
          }
        }
    });
    try {
      m_latch.await();
    }
    catch (InterruptedException ie) {
      ie.printStackTrace();
    }
    java.awt.EventQueue.invokeLater(new Runnable() {
        public void run() {
          System.out.println("tf len: " + m_ta.getText().length());
          System.out.println("doc len: " + m_doc.getLength());
          System.exit(0);
        }
    });
  }
}

+1 对你的坚韧印象深刻 :-) 尽管你在某些地方让我迷失了方向(这是我的错,因为没有真正的动力去深入研究,不是你的错!) - 通常,我只需确保在EDT上访问所有与swing相关的内容并感到高兴。至于插入符号:DefaultCaret有一个updatedPolicy属性(或类似的属性,懒得搜索...咳咳),在大多数用例中已经足够好了。 - kleopatra
@Kleopatra,谢谢!你提醒了我一个重要的事情。这一切是为了什么?效率。结果并不令人印象深刻(对于10000个循环,在Linux上):工作线程(就像上面的代码):10秒,invokeLater:15.5秒,invokeAndWait:59.5秒。invokeLater只有大约50%的性能损失,所以我同意你的看法!副作用:EDT解决方案会自动更新插入符号。 - Jarekczek
关于滚动到最后一行:您可以自动完成此操作;请查看默认插入符号更新策略:http://docs.oracle.com/javase/8/docs/api/javax/swing/text/DefaultCaret.html#setUpdatePolicy-int- - Mark VY

2

JTextArea.append(..) 是线程安全的,因此可以放心地从不同的线程调用它。

然而,.append() 的 javadoc 中指出:

Does nothing if the model is null or the string is null or empty.

因此,请确保通过适当的构造函数初始化JTextArea模型。

2

JTextArea线程安全吗?

一般情况下不是线程安全的。正如其他人所说,即使append方法也不再被文档记录为线程安全。然而,Java 7 AbstractDocument.insertString的文档明确指出该方法是线程安全的。

使用AbstractDocument.insertString根据文档似乎是安全的。这也是唯一合理的选择。在GUI线程中更新字符串模型将导致严重的性能损失。

JTextArea.append呢?我认为它取决于底层的Document。对于PlainDocumentDefaultStyledDocument,它可能是线程安全的。对于其他模型,应查看相关文档。如果不知道底层文档是什么,则应将append视为非线程安全,并仅从EDT调用它。

编辑:append不是线程安全的另一个可能原因是它由2个操作组成:getLengthinsertString,在两者之间,文档的内容可能会发生更改。因此,使用像insertString(getLength(), ...)这样的结构也要小心。没有同步是不正确的。 AbstractDocument.writeLock可能有所帮助,但它受到保护。


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