23

我经常听到有人批评 Swing 库中缺乏线程安全性。然而,我不确定我在自己的代码中会做什么可能会导致问题:

Swing 不是线程安全的事实在什么情况下发挥作用?

我应该积极避免做什么?

4

11 回答 11

27
  1. 永远不要为响应按钮、事件等而执行长时间运行的任务,因为它们位于事件线程上。如果您阻止事件线程,整个 GUI 将完全没有响应,从而导致用户非常生气。这就是为什么 Swing 看起来缓慢而硬朗的原因。

  2. 使用线程、执行器和 SwingWorker 来运行不在 EDT 上的任务(事件调度线程)。

  3. 不要在 EDT 之外更新或创建小部件。您可以在 EDT 之外执行的唯一调用是 Component.repaint()。使用 SwingUtilitis.invokeLater 确保某些代码在 EDT 上执行。

  4. 使用EDT 调试技术和智能外观(如检查 EDT 违规的Substance

如果您遵循这些规则,Swing 可以制作一些非常吸引人且响应迅速的 GUI

一些非常棒的 Swing UI 工作示例:Palantir Technologies。注意:我不为他们工作,只是一个很棒的挥杆的例子。可惜没有公开演示......他们的博客也很好,稀疏,但很好

于 2008-10-08T12:12:28.503 回答
11

这是让我很高兴我购买了Robinson & Vorobiev 关于 Swing 的书的问题之一。

任何访问 a 状态的东西java.awt.Component都应该在 EDT 中运行,除了三个例外: 任何特别记录为线程安全的东西,例如repaint()revalidate()invalidate(); UI 中尚未实现的任何组件;start()以及在该 Applet被调用之前的 Applet 中的任何组件。

特制线程安全的方法非常少见,通常只需记住它们就足够了;您通常也可以假设没有这样的方法(例如,将重绘调用包装在 SwingWorker 中是完全安全的)。

已实现意味着该组件是一个顶级容器(如 JFrame),其中任何一个setVisible(true)show()pack()已被调用,或者它已被添加到已实现的组件中。这意味着在 main() 方法中构建你的 UI 是非常好的,就像许多教程示例所做的那样,因为它们不会调用setVisible(true)顶级容器,直到每个组件都被添加到它,字体和边框配置等等。

出于类似的原因,在它的init()方法中构建你的小程序 UI 是非常安全的,然后start()在它全部构建后调用。

在 Runnables 中包装后续组件更改以发送到invokeLater(),只需执行几次即可轻松完成。我觉得烦人的一件事是someTextField.getText()从另一个线程读取组件的状态(例如,)。从技术上讲,这也必须包含在 中invokeLater();在实践中,它可以使代码很快变得丑陋,而且我通常不会打扰,或者我很小心地在初始事件处理时获取该信息(无论如何,在大多数情况下通常是正确的时间)。

于 2008-10-08T21:28:46.197 回答
8

不仅 Swing 不是线程安全的(不是很多),而且它是线程敌对的。如果您开始在单个线程(EDT 除外)上执行 Swing 工作,那么当 Swing 切换到 EDT(未记录)时,很可能会出现线程安全问题。即使是旨在实现线程安全的 Swing 文本也不是很有用的线程安全(例如,要附加到文档,您首先需要找到长度,该长度可能会在插入之前更改)。

因此,在 EDT 上执行所有 Swing 操作。请注意,EDT 不是调用 main 的线程,因此请启动您的(简单)Swing 应用程序,如下面的样板:

class MyApp {
    public static void main(String[] args) {
        java.awt.EventQueue.invokeLater(new Runnable() { public void run() {
            runEDT();
        }});
    }
    private static void runEDT() {
        assert java.awt.EventQueue.isDispatchThread();
        ...
于 2008-10-08T13:09:43.100 回答
4

使用类似物质的智能皮肤的另一种方法是创建以下实用方法:

public final static void checkOnEventDispatchThread() {
    if (!SwingUtilities.isEventDispatchThread()) {
        throw new RuntimeException("This method can only be run on the EDT");
    }
}

在您编写的每个需要在事件调度线程上的方法中调用它。这样做的一个优点是可以非常快速地禁用和启用系统范围的检查,例如可能在生产中删除它。

请注意,智能皮肤当然可以提供额外的覆盖范围以及仅此而已。

于 2009-12-21T14:52:45.847 回答
3

除了在事件分派线程上,积极避免做任何 Swing 工作。Swing 被编写为易于扩展,Sun 认为单线程模型对此更好。

在遵循上述建议时,我没有遇到任何问题。在某些情况下,您可以从其他线程“摇摆”,但我从未发现需要。

于 2008-10-08T12:04:50.950 回答
2

如果您使用的是 Java 6,那么 SwingWorker 无疑是处理这个问题的最简单方法。

基本上,您要确保在 EventDispatchThread 上执行任何更改 UI 的操作。

这可以通过使用 SwingUtilities.isEventDispatchThread() 方法告诉您是否在其中找到(通常不是一个好主意 - 您应该知道哪个线程处于活动状态)。

如果您不在 EDT 上,则使用 SwingUtilities.invokeLater() 和 SwingUtilities.invokeAndWait() 在 EDT 上调用 Runnable。

如果你在 EDT 上更新 UI,你会得到一些非常奇怪的行为。就我个人而言,我不认为这是 Swing 的缺陷,通过不必同步所有线程来提供 UI 更新,您可以获得一些不错的效率 - 您只需要记住这个警告。

于 2008-10-08T13:45:52.637 回答
2

“线程不安全”这句话听起来好像有一些本质上不好的东西(你知道......“安全” - 好;“不安全” - 坏)。现实情况是,线程安全是有代价的——线程安全对象的实现通常要复杂得多(尽管 Swing 已经足够复杂了。)

此外,使用锁定(慢)或比较和交换(复杂)策略来实现线程安全。鉴于 GUI 与人类的交互往往不可预测且难以同步,因此许多工具包决定通过单个事件泵来引导所有事件。这适用于 Windows、Swing、SWT、GTK 和其他可能的应用程序。实际上,我不知道一个真正线程安全的 GUI 工具包(这意味着您可以从任何线程操作其对象的内部状态)。

相反,通常做的是 GUI 提供了一种处理线程不安全的方法。正如其他人所指出的,Swing 总是提供有点简单的 SwingUtilities.invokeLater()。Java 6 包括出色的 SwingWorker(可从 Swinglabs.org 获得以前的版本)。还有像 Foxtrot 这样的第三方库,用于在 Swing 上下文中管理线程。

Swing 的臭名昭著是因为设计人员采取了轻率的做法,即假设开发人员会做正确的事情,不会拖延 EDT 或从 EDT 外部修改组件。他们已经大声而清晰地说明了他们的线程策略,开发人员可以遵循它。

让每个 swing API 为每个属性集、无效等向 EDT 发布作业是微不足道的,这将使其线程安全,但代价是大幅减速。您甚至可以使用 AOP 自己完成。作为比较,当从错误的线程访问组件时,SWT 会抛出异常。

于 2008-10-09T02:25:36.033 回答
1

这是一个模式,可以让你轻松地挥动线程。

Sublass Action (MyAction) 并使其成为 doAction 线程。使构造函数采用字符串名称。

给它一个抽象的 actionImpl() 方法。

让它看起来像.. (伪代码警告!)

doAction(){
new Thread(){
   public void run(){
    //kick off thread to do actionImpl().
       actionImpl();
       MyAction.this.interrupt();
   }.start();  // use a worker pool if you care about garbage.
try {
sleep(300);
Go to a busy cursor
sleep(600);
Show a busy dialog(Name) // name comes in handy here
} catch( interrupted exception){
  show normal cursor
}

您可以记录任务所花费的时间,下一次,您的对话框可以显示一个不错的估计。

如果你想变得非常好,也可以在另一个工作线程中睡觉。

于 2008-10-09T01:00:29.670 回答
1

请注意,即使模型接口也不是线程安全的。使用单独的 get 方法查询大小和内容,因此无法同步它们。

从另一个线程更新模型的状态允许它至少绘制大小仍然更大(表格行仍然存在)但内容不再存在的情况。

始终在 EDT 中更新模型的状态可以避免这些。

于 2008-10-11T20:00:26.923 回答
1

当您从不是 EDT 的任何线程与 GUI 组件进行任何交互时,确实必须使用 invokeLater() 和 invokeAndWait()。

它可能在开发过程中起作用,但像大多数并发错误一样,您会开始看到奇怪的异常出现,这些异常似乎完全不相关,并且是非确定性的——通常是在真实用户发布后发现的。不好。

此外,您不相信您的应用程序将继续在具有越来越多内核的未来 CPU 上运行 - 由于它们是真正并发的而不是仅由操作系统模拟,因此更容易遇到奇怪的线程问题。

是的,将每个方法调用都包装回 EDT 中的 Runnable 实例会变得很丑陋,但这对你来说就是 Java。在我们关闭之前,您只需要忍受它。

于 2008-10-16T06:00:31.170 回答
1

有关线程的更多详细信息,Allen Holub 的 Taming Java Threads 是一本较旧的书,但值得一读。

Holub,真正促进响应式 UI 和详细示例以及如何缓解问题。

http://www.amazon.com/Taming-Java-Threads-Allen-Holub/dp/1893115100 http://www.holub.com/software/taming.java.threads.html

喜欢最后的“如果我是国王”部分。

于 2009-05-01T06:11:48.597 回答