如果Swing模型的getter不是线程安全的,你如何处理它们?

众所周知,更新Swing GUI必须仅在EDT中完成。 较少的广告是从GUI中读取内容必须/也应该在EDT中完成。 例如,让我们使用ButtonModel的isSelected()方法,它告诉(例如)ToggleButton的状态(“向下”或“向上”)。

在我看过的每个例子中, isSelected()从主线程或任何线程中被大量查询。 但是当我查看DefaultButtonModel的实现时,它没有同步,并且值不是volatile。 所以,严格来说,如果isSelected()从任何其他线程读取而不是从它设置的线程(当用户按下按钮时是EDT isSelected() ,则返回垃圾。 还是我弄错了?

当Bloch的Effective Java中的第66项震惊时,我最初想到这个,这个例子:

 public class StopThread { private static boolean stopRequested; public static void main(String[] args) throws InterruptedException { Thread backgroundThread = new Thread(new Runnable() { public void run() { int i = 0; while(!stopRequested) i++; } }); backgroundThread.start(); TimeUnit.SECONDS.sleep(1); stopRequested = true; } } 

与看起来相反,该程序永远不会终止,至少在某些机器上。 从主线程更新stopRequested标志对后台线程是不可见的。 可以使用synchronized getter和setter修复情况,或者通过设置标志volatile来修复。

所以:

  1. 在EDT之外查询Swing模型的状态(严格来说)是错误的吗?
  2. 如果没有,怎么样?
  3. 如果是的话,你如何处理它? 运气好,还是通过一些聪明的解决方法? InvokeAndWait?

  1. 不,没有错,但与任何跨线程通信一样,如果您决定这样做(例如使用synchronizedvolatile ),您需要确保提供适当的线程安全机制。 例如,我通常编写自己的TableModel实现,通常位于List ,其中X是我的业务对象。 如果我打算让其他线程查询List,我将使它成为一个同步Collection 。 值得注意的是,我通常不会从其他线程更新List ; 只查询它。
  2. 因为它与任何其他multithreading应用程序完全相同。
  3. 我通常使集合同步(见1)。

警告

尽管我上面的回答,我通常直接访问EDT之外的模型。 正如Carl所提到的,如果通过某些GUI动作调用执行更新的动作,则不需要执行任何同步,因为EDT已经运行了代码。 但是,如果我希望执行一些导致模型被更改的后台处理,我通常会调用一个SwingWorker ,然后在done()方法中(即在EDT上)分配doInBackground()的结果。 这是更清洁的恕我直言,因为doInBackground()方法没有副作用。

是的,这是错的; 除非两个线程在同一个对象上同步或者使用其他一些内存屏障,比如由JMM定义的volatile,它们可以观察到不一致的内存内容。 期。 故事结局。 违反这一规定可能适用于某些甚至许多架构,但它最终会让你陷入困境。

这里的问题是,除了你提供模型的一些例外,例如Adamski所提到的,Swing代码不会同步任何东西。

值得注意的是JMM(Java内存模型)已经在Java 5中以非常重要的方式改变了JSR-133,因此使用J4和早期JVM的行为可能会产生与J5及更高版本不同的结果。 正如您所料,修订后的JMM大大改善了。

Swing通常不仅不是线程安全的,而且是线程敌对的。 但是,除了Swing文本之外,大多数模型都是线程无关的。 这意味着您可以在任何线程中使用模型,前提是您使用标准线程保护。 Swing文本试图是线程安全的但是失败了,实际上是线程敌对的。 仅使用事件调度线程(EDT)中的Document

通常的建议是在EDT上运行可能长时间运行(通常是阻塞)的任务。 但是,线程很难。 SwingWorker是一种引发糟糕设计的流行方式 – 避免它。 尽量保持EDT和非EDT代码分开。 尽量不要分享可变状态。

线程错误很难。 您可能无法在计算机上看到它们,但客户可能会这样做。 由于JRE的更高版本中的优化,可能会出现错误。 线程错误很难跟踪。 因此,要保守。 即使短暂地阻止EDT也比讨厌的bug更好。

  1. 我从不担心它,但严格地说,是的,这可能是错的。
  2. N / A。
  3. 我更改了我自己构造的GUI更新(通过监听器)模型,这些模型不是由Swing包含或构造的。 由于GUI更新了模型,因此无需询问GUI。

由于我自己创建模型(这可能非常简单,例如带有getter / setter和PropertyChangeSupport的String),如果需要,我可以使访问器同步。 由于线程争用等原因,我很少遇到片状行为。

如果是的话,你如何处理它? 运气好,还是通过一些聪明的解决方法? InvokeAndWait?

其他人已经提到过SwingWorker ,你已经知道了invoke*方法。 javax.swing.Timer也很有用。 安全的multithreading编程没有银子弹,虽然好的设计会有很长的路要走。

您可以采取一些措施来检测错误。 如果您设计的方法只能从事件派发线程调用,请保护它,以便在有人尝试从另一个线程访问时它将失败:

  public static class ThreadUnsafeAccessException extends RuntimeException { private static final long serialVersionUID = 1L; } @Override public Object getElementAt(int index) { if (!SwingUtilities.isEventDispatchThread()) { throw new ThreadUnsafeAccessException(); } return decorated.getElementAt(index); } 

将这种方法改造为在主应用程序线程上进行大量设置的现有应用程序可能意味着一些重大的重构。

这是一个很好的问题,Software Monkey有正确的答案。 如果要进行有效的线程,则必须完全理解Java 内存 模型 (JMM)。 请注意,java内存模型已经使用JSR-133进行了更改 ,因此您发现的许多旧的线程示例都是错误的。 例如,volatile关键字已从几乎无用变为现在几乎与synchronized关键字一样强制执行。

此外,我们现在拥有真正的,无处不在的多核cpu,这意味着真正的multithreading。 直到几年前的multithreading只是伪装在单核cpu上,这就是为什么那么多糟糕的示例代码。 它曾经工作过。 现在它不会。

说真的,如果Joshua Bloch能够弄错,那就要小心翼翼。 这是复杂的东西(是的,他的代码是错误的。使变量变得易变,但它在所有情况下都会起作用)。

哦,是的,Adamski说得对。 使用SwingWorker确保在后台线程中完成长时间运行的代码,并且在完成操作数据时,它应该在done()方法上访问Swing。 如果您需要阅读,请制作SwingWorker 之前完成并final完成信息。

将在EDT上调用的代码示例,最有可能在ActionListener或somesuch(psuedocode)中调用:

 public void someOverlySimpleExampleThatIsCalledOnEDT() { /* * Any code run here is guaranteed to happen-before * the start of a background thread. */ final String text = myTextField().getText(); SwingWorker sw = new SwingWorker() { private volatile List data; public Object doInBackground() { //happens in background thread. No Swing access data = myDao.getDataFromInternet(text); return null; } public void done() { //Happens in EDT. Swing away Merrill loadDataIntoJTable(data); } } sw.execute(); /* * Any code run here happens concurrently with doInBackground(). Be careful. * Usually leave empty */ }