Java: тестирование доступа потоков к небезопасным методам

Моя стратегия решения проблем многопоточности в Java-приложении Swing состоит в том, чтобы разделить методы на три типа:

  1. методы, к которым должен обращаться поток графического интерфейса. Эти методы никогда не должны блокировать и могут вызывать методы свинга. Не потокобезопасный.
  2. методы, к которым должны иметь доступ потоки, не связанные с графическим интерфейсом. В основном это касается всех (потенциально) блокирующих операций, таких как доступ к диску, базе данных и сети. Они никогда не должны вызывать методы свинга. Не потокобезопасный.
  3. методы, к которым могут получить доступ оба. Эти методы должны быть потокобезопасными (например, синхронизированными).

Я думаю, что это правильный подход для приложений с графическим интерфейсом, где обычно всего два потока. Устранение проблемы действительно помогает уменьшить «площадь поверхности» для условий гонки. Предостережение, конечно, заключается в том, что вы никогда случайно не вызовете метод из неправильного потока.

У меня вопрос о тестировании:

Существуют ли инструменты тестирования, которые могут помочь мне проверить, что метод вызывается из правильного потока? Я знаю о SwingUtilities.isEventDispatchThread(), но я действительно ищу что-то, использующее аннотации Java или аспектно-ориентированное программирование, чтобы мне не приходилось вставлять один и тот же шаблонный код в каждый метод программы.


person amarillion    schedule 04.09.2009    source источник
comment
+1 за творческий вопрос   -  person KLE    schedule 04.09.2009
comment
synchronized не равно потокобезопасному. Я настоятельно рекомендую вам прочитать о «новых» библиотеках параллелизма в java 5, особенно я думаю, что Futures полезны для разработки Swing.   -  person Jens Schauder    schedule 04.09.2009
comment
@Jens: ты прав, я немного отредактировал вопрос.   -  person amarillion    schedule 04.09.2009


Ответы (4)


Здесь есть запись в блоге с несколькими решениями для проверка нарушений EDT. Один из них - это настраиваемый менеджер перерисовки, а также есть решение AspectJ. Раньше я использовал диспетчер перерисовки и нашел его весьма полезным.

person Mark    schedule 04.09.2009

Спасибо за все советы, вот решение, которое я в итоге придумал. Это оказалось проще, чем я думал. В этом решении используются как AspectJ, так и аннотации. Это работает так: просто добавьте одну из аннотаций (определенных ниже) к методу или классу, и простая проверка на нарушения правил 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 {}

После этого я определил Аспект, который выполняет фактическую проверку:

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), и вы скоро узнаете, где есть нарушения, если таковые имеются.

Для справки вот решение Александра Поточкина, о котором говорил Марк. Это аналогичный подход, но он проверяет вызовы методов 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();
      }
    }
}
person amarillion    schedule 06.09.2009

Из того, что я прочитал, у вас уже есть конкретное решение, вам нужно только сократить необходимый шаблонный код.

Я бы использовал технологию перехвата.

В наших проектах используется Spring, и мы легко создаем перехватчик, который проверяет это условие перед каждым вызовом. Только во время тестирования мы будем использовать конфигурацию Spring, которая создает перехватчик (мы можем повторно использовать обычную конфигурацию Spring, просто добавив к ней).

Чтобы узнать, какой регистр мы должны использовать для метода, вы можете, например, прочитать аннотацию или использовать другое средство конфигурации.

person KLE    schedule 04.09.2009

Безусловно, наиболее важным является обеспечение четкого разделения между EDT и не-EDT. Установите четкую границу между ними. Нет классов (SwingWorker, я смотрю на вас) с методами в обоих лагерях. Это относится к потокам в целом. Странный assert java.awt.EventQueue.isDispatchThread(); хорош рядом с интерфейсами между потоками, но не зацикливайтесь на нем.

person Tom Hawtin - tackline    schedule 04.09.2009