Статьи

Тестирование многопоточного кода в Java

Тестирование многопоточного кода — сложная задача. Первый совет, который вы получите при попытке проверить параллелизм, — максимально изолировать ваши параллельные проблемы в коде. Это общий совет по дизайну, но в этом случае он еще важнее. Убедитесь, что сначала правильно проверен блок логики, который обернут параллельной конструкцией. В противном случае вы могли бы потратить много времени, пытаясь выяснить проблему параллелизма, которая в итоге оказывается ошибочной бизнес-логикой. picot.png

Как только вы это осветите, вы можете подумать о своей стратегии тестирования параллельных систем. ГСНО рассказывает, как вы можете это сделать. Здесь вы можете найти код, который я собираюсь объяснить:

Во-первых, давайте посмотрим на тестируемую систему:

01
02
03
04
05
06
07
08
09
10
11
public class AtomicBigCounter {
      private BigInteger count = BigInteger.ZERO;
 
      public BigInteger count() {
          return count;
      }
 
      public void inc() {
          count = count.add(BigInteger.ONE);
      }
  }

Как видите, этот класс не является потокобезопасным, так как он предоставляет некоторое состояние с помощью метода inc (). Состояние не является потокобезопасным (вы можете использовать AtomicInteger вместо BigInteger, чтобы исправить это). Чтобы протестировать этот класс, мы включим непараллельный и параллельный тест.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
@Test public void canIncreaseCounter(){
    ...
}
 
@Test public void canIncrementCounterFromMultipleThreadsSimultaneously()
    throws  InterruptedException {
    MultithreadedStressTester stressTester = new MultithreadedStressTester(25000);
 
    stressTester.stress(new Runnable() {
        public void run() {
            counter.inc();
        }
    });
 
    stressTester.shutdown();
 
    assertThat("final count", counter.count(),
    equalTo(BigInteger.valueOf(stressTester.totalActionCount())));
}

Стресс-тестер будет выполнять метод n петель с m нитями. Поскольку наш метод увеличивается на единицу, мы должны увидеть, что n*m равно counter.count() .

Интересным классом является MultithreadedStressTester, хотя:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public void stress(final Runnable action) throws InterruptedException {
    spawnThreads(action).await();
}
 
private CountDownLatch spawnThreads(final Runnable action) {
    final CountDownLatch finished = new CountDownLatch(threadCount);
 
    for (int i = 0; i < threadCount; i++) {
        executor.execute(new Runnable() {
            public void run() {
                try {
                    repeat(action);
                }
                finally {
                    finished.countDown();
                }
            }
        });
    }
    return finished;
}
 
private void repeat(Runnable action) {
    for (int i = 0; i < iterationCount; i++) {
        action.run();
    }
}

Если вы выполните этот тест, вы получите другие результаты, а иногда даже проходящие! Это потому, что этот тест не является детерминированным, мы не можем гарантировать, как потоки будут чередоваться при каждом выполнении. Если мы хотим быть как можно более уверенными в том, что этот тест найдет возможную ошибку, мы должны увеличить количество потоков и итераций, но с очевидным временным компромиссом.

Вы можете использовать более детерминированный подход, используя Weaver . Чтобы понять, как это работает, давайте проиллюстрируем это на примере. Допустим, у нас есть хранилище в памяти и не поточно-ориентированное хранилище:

1
private final Map<Level, Scores> scoresByLevel;

У нас есть некоторый сервис, который обращается к хранилищу, обертывающему этот магазин:

1
2
3
4
5
6
1 Optional<Scores> scoresFromStore = scoreRepo.findBy(score.level());
  2       if(scoresFromStore.isPresent()) {
  3          scoreRepo.update(score.level(), score);
  4       } else {
  5          scoreRepo.save(score.level(), new Scores().add(score));
  6       }

Этот сервис представляет собой одноэлементный сервер, который создает поток для каждого запроса, поэтому мы хотели бы выполнить этот фрагмент атомарно. Мы могли бы использовать недетерминированный подход к стресс-тесту или использовать Weaver. Если глубоко задуматься над этой проблемой, мы поймем, что хотим протестировать каждую комбинацию из следующих (например, поток 1 выполняет строку 1 в момент x, а поток 2 выполняет строку 1 в момент x, будет -> T1 / 1: Т2 / 1)

  • T1 / 1: T2 / 1
  • T1 / 1: T2 / 2
  • T1 / 1: T2 / 3
  • ….
  • T1 / 2: T2 / 1
  • T1 / 2: T2 / 2
  • T1 / 2: T2 / 3
  • ….

Например, у нас будет проблема, если T1 / 5 и T2 / 2, так как T1 еще не сохранил, а T2 уже получил пустой счет из магазина. Это означает, что T1 сохранит счет на уровне, а затем T2 сделает то же самое, нарушив логику. И это именно то, что делает Уивер, он захватывает метод и выполняет вышеуказанные комбинации, используя два потока.

Если я избавлюсь от подготовительного кода (помеченного @ThreadedBefore), тестовый код будет выглядеть так:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
@ThreadedMain
public void mainThread() {
    scoreService.save(LEVEL_ID, SCORE_VALUE, aUser);
}
 
@ThreadedSecondary
public void secondThread() {
    scoreService.save(LEVEL_ID, ANOTHER_SCORE_VALUE, aUser);
}
 
@ThreadedAfter
public void after() {
    Optional<Scores> scores = scoreRepo.findBy(aLevel());
    assertThat(scores.isPresent()).isTrue();
    assertThat(scores.get().contains(aScoreWith(aUser))).isTrue();
    assertThat(scores.get().contains(aDifferentScoreWith(aUser))).isTrue();
}
 
@Test
public void testThreading() {
    new AnnotatedTestRunner().runTests(this.getClass(), ScoreService.class);
}

Этот тест всегда будет неудачным, поскольку он является детерминированным. Как вы можете видеть, тестирование параллелизма довольно сложно, и именно поэтому я сторонник современных сред, которые пытаются скрыть эти трудности в платформе или преодолеть проблему с помощью неизменных данных.

  • Вы можете прочитать больше об этом здесь .
Ссылка: Тестирование многопоточного кода на Java от нашего партнера по JCG Фелипе Фернандеса в блоге Crafted Software .