Java:测试线程访问“非线程安全”方法

9

我在Swing Java应用程序中处理线程问题的策略是将方法分为三种类型:

  1. 只能由GUI线程访问的方法。这些方法不应阻塞,可以调用Swing方法。不是线程安全的。
  2. 只能由非GUI线程访问的方法。基本上适用于所有(可能)会阻塞的操作,如磁盘、数据库和网络访问。它们不应调用Swing方法。不是线程安全的。
  3. 两种线程都可以访问的方法。这些方法必须是线程安全的(例如同步)。

我认为这是GUI应用程序的有效方法,因为通常只有两个线程。分解问题确实有助于减少竞争条件的“表面积”。当然,缺点是您永远不会意外地从错误的线程调用方法。

我的问题是关于测试:

是否有测试工具可以帮助我检查方法是否从正确的线程调用?我知道SwingUtilities.isEventDispatchThread(),但我真正寻找的是使用Java注释或面向方面编程的东西,以便我不必在程序的每个方法中插入相同的样板代码。


+1 for creative question - KLE
同步不等于线程安全。我强烈建议您阅读Java 5中的“新”并发库,特别是Futures似乎对Swing开发很有用。 - Jens Schauder
@Jens:你说得对,我稍微编辑了一下这个问题。 - amarillion
4个回答

2
感谢所有的提示,这是最终我想出的解决方案。它比我想象的要简单。这个解决方案同时使用了AspectJ和Annotations。它的工作原理是这样的:只需将以下注释(定义如下)之一添加到方法或类中,并在开头插入一个简单的EDT规则违规检查。特别是如果像这样标记整个类,您可以仅使用极少量的额外代码进行大量测试。
首先,我下载了AspectJ并将其添加到我的项目中(在Eclipse中,您可以使用AJDT)。
然后,我定义了两个新的注释:
import java.lang.annotation.ElementType;
import java.lang.annotation.Target;

/**
 * Indicates that this class or method should only be accessed by threads
 * other than the Event Dispatch Thread
 * <p>
 * Add this annotation to methods that perform potentially blocking operations,
 * such as disk, network or database access. 
 */
@Target({ElementType.METHOD, ElementType.TYPE, ElementType.CONSTRUCTOR})
public @interface WorkerThreadOnly {}

并且

import java.lang.annotation.ElementType;
import java.lang.annotation.Target;

/**
 * Indicates that this class or method should only be accessed by the 
 * Event Dispatch Thread
 * <p>
 * Add this annotation to methods that call (swing) GUI methods
 */
@Target({ElementType.METHOD, ElementType.TYPE, ElementType.CONSTRUCTOR})
public @interface EventDispatchThreadOnly {}

之后,我定义了执行实际检查的Aspect:
import javax.swing.SwingUtilities;

/** Check methods / classes marked as WorkerThreadOnly or EventDispatchThreadOnly */
public aspect ThreadChecking {

    /** you can adjust selection to a subset of methods / classes */
    pointcut selection() : execution (* *(..));

    pointcut edt() : selection() && 
        (within (@EventDispatchThreadOnly *) ||
        @annotation(EventDispatchThreadOnly));

    pointcut worker() : selection() && 
        (within (@WorkerThreadOnly *) ||
        @annotation(WorkerThreadOnly));

    before(): edt() {
        assert (SwingUtilities.isEventDispatchThread());
    }

    before(): worker() {
        assert (!SwingUtilities.isEventDispatchThread());
    }
}

现在在应该线程限制的方法或类中添加@EventDispatchThreadOnly或@WorkerThreadOnly。不要在线程安全的方法中添加任何内容。
最后,只需启用断言(JVM选项-ea),您很快就会发现违规行为在哪里。
供参考,这是Mark所提到的Alexander Potochkin的解决方案。它是一种类似的方法,但它检查您的应用程序对Swing方法的调用,而不是您的应用程序内部的调用。这两种方法是互补的,可以一起使用。
import javax.swing.*;

aspect EdtRuleChecker {
    private boolean isStressChecking = true;

    public pointcut anySwingMethods(JComponent c):
         target(c) && call(* *(..));

    public pointcut threadSafeMethods():         
         call(* repaint(..)) || 
         call(* revalidate()) ||
         call(* invalidate()) ||
         call(* getListeners(..)) ||
         call(* add*Listener(..)) ||
         call(* remove*Listener(..));

    //calls of any JComponent method, including subclasses
    before(JComponent c): anySwingMethods(c) && 
                          !threadSafeMethods() &&
                          !within(EdtRuleChecker) {
     if(!SwingUtilities.isEventDispatchThread() &&
         (isStressChecking || c.isShowing())) 
     {
             System.err.println(thisJoinPoint.getSourceLocation());
             System.err.println(thisJoinPoint.getSignature());
             System.err.println();
      }
    }

    //calls of any JComponent constructor, including subclasses
    before(): call(JComponent+.new(..)) {
      if (isStressChecking && !SwingUtilities.isEventDispatchThread()) {
          System.err.println(thisJoinPoint.getSourceLocation());
          System.err.println(thisJoinPoint.getSignature() +
                                " *constructor*");
          System.err.println();
      }
    }
}

2
这是一篇博客文章,关于检查EDT违规的几种解决方案。其中一种是自定义重绘管理器,还有一个AspectJ的解决方案。我过去使用过重绘管理器,发现它非常有用。点击此处阅读完整文章。

1

根据我所读的,您已经有了一个具体的解决方案,只是想减少所需的样板代码。

我会使用拦截技术。

我们的项目使用Spring,并且我们很容易创建一个拦截器,在每次调用之前检查此条件。在仅测试期间,我们将使用创建拦截器的Spring配置(我们可以重用常规Spring配置,只需添加即可)。

为了知道应该为方法使用哪种情况,您可以阅读注释,或使用其他配置方式。


1
迄今为止最重要的事情是确保EDT和非EDT之间有明确的分离。在两者之间放置一个清晰的接口。不要在两个领域都有方法的类(我指的是SwingWorker)。这适用于线程的一般情况。在线程之间的接口附近加上assert java.awt.EventQueue.isDispatchThread();会很好,但不要过分纠结。

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