简体   繁体   中英

Java: testing thread access to “not thread-safe” methods

My strategy for threading issues in a Swing Java app is to divide methods in three types:

  1. methods that should be accessed by the GUI thread. These methods should never block and may call swing methods. Not thread-safe.
  2. methods that should be accessed by non-GUI threads. Basically this goes for all (potentially) blocking operations such as disk, database and network access. They should never call swing methods. Not thread-safe.
  3. methods that could be accessed by both. These methods have to be thread-safe (eg synchronized)

I think this is a valid approach for GUI apps, where there are usually only two threads. Cutting up the problem really helps to reduce the "surface area" for race conditions. The caveat of course is that you never accidentally call a method from the wrong thread.

My question is about testing:

Are there testing tools that can help me check that a method is called from the right thread? I know about SwingUtilities.isEventDispatchThread() , but I'm really looking for something using Java annotations or aspect-oriented programming so that I don't have to insert the same boilerplate code in each and every method of the program.

Here is a blog entry with a few solutions for checking EDT violations. One is a custom repaint manager and there is also an AspectJ solution. I have used the repaint manager in the past and found it quite useful.

Thanks for all the tips, here is the solution I came up with in the end. It was easier than I thought. This solution uses both AspectJ and Annotations. It works like this: simply add one of the annotations (defined below) to a method or a class, and a simple check for EDT rule violations will be inserted into it at the beginning. Especially if you mark whole classes like this, you can do a whole lot of testing with only a tiny amount of extra code.

First I downloaded AspectJ and added it to my project (In eclipse you can use AJDT )

Then I defined two new Annotations:

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 {}

and

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 {}

After that, I defined the Aspect that does the actual checking:

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());
    }
}

Now add @EventDispatchThreadOnly or @WorkerThreadOnly to the methods or classes that should be thread-confined. Don't add anything to thread safe methods.

Finally, Simply run with assertions enabled (JVM option -ea) and you'll find out soon enough where the violations are, if any.

For reference purposes, here is the solution of Alexander Potochkin , which Mark referred to. It's a similar approach, but it checks calls to swing methods from your app, instead of calls within your app. Both approaches are complimentary and can be used together.

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();
      }
    }
}

From what I read, you already have a specific solution, you only want to reduce the boilerplate code that is required.

I would use an interception technology.

Our projects use Spring, and we easily create an Interceptor that checks this condition before each call. During testing-only, we would use a Spring configuration that creates an interceptor (we can reuse the regular Spring config, just add to it).

For knowing what case we should use for a method, you can read the annotation for example, or use another configuration mean.

By far the most important thing to do is to ensure that there is a clear separation between EDT and non-EDT. Put a clear interface between the two. Don't have classes ( SwingWorker , I'm looking at you) with methods in both camps. This applies to threads in general. The odd assert java.awt.EventQueue.isDispatchThread(); is nice near interfaces between threads, but don't get hung up on it.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM