Рубрики
Без рубрики

Как писать многопоточные тесты на Java

Руководство по написанию теста, использующего несколько потоков, т.Е. для проверки блокировок базы данных. Помечено как программирование, тестирование, база данных, java.

Проведение выходных дома дает вам бесконечные возможности. Мы, например, играли в настольные игры, смотрели онлайн-курсы и даже восстанавливали Стоунхендж из глины. Но мы не являемся многоядерными процессорами, которые могут выполнять все эти задачи одновременно (параллелизм). Наш человеческий объем внимания больше похож на поток, который должен эффективно переключаться с задачи на задачу (параллелизм). А смешивание глины блокирует мой разум практически от любой другой деятельности!

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

Установка

Представьте себе, что у вас есть ресурс, к которому может получить доступ практически каждый, но к которому не должны обращаться двое одновременно. Скажите: туалетная бумага в супермаркете.

@Entity
@Data
public class ToiletPaper {
    @Id
    @GeneratedValue
    long id;
    boolean available = true;
}

Как вы видите, наша туалетная бумага просто идентифицируется по идентификатору и содержит информацию о том, доступна она или нет.

Всегда хорошо иметь хранилище, полное туалетной бумаги! Мы хотим упростить его и определить две функции, которые извлекают доступную пачку туалетной бумаги. Единственная разница заключается в том, что будет реализована ПЕССИМИСТИЧЕСКАЯ блокировка ЗАПИСИ.

@Repository
public interface ToiletPaperRepository extends JpaRepository {

    Optional findTopByAvailableTrue();

    @Lock(LockModeType.PESSIMISTIC_WRITE)
    Optional findFirstByAvailableTrue();
}

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

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

@Transactional
public ToiletPaper grabToiletPaper() {
    return toiletPaperRepository.findTopByAvailableTrue()
            .map(this::updateToiletPaperToUnavailable)
            .orElseThrow(OutOfToiletPaperException::new);
}

private ToiletPaper updateToiletPaperToUnavailable(ToiletPaper toiletPaper) {
    toiletPaper.setAvailable(false);
    return toiletPaperRepository.save(toiletPaper);
}

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

Многопоточный тест

Теперь давайте смоделируем две реальные ситуации в супермаркете:

  • Все одновременно тянутся за туалетной бумагой.
  • Если кто-то получает доступ к туалетной бумаге, к ней не может получить доступ кто-то другой.

Каждый покупатель супермаркета будет представлен нитью. И инструкции, которые он будет выполнять, определены в классе, называемом Жадный бегун или PatientRunner что оба будут реализовывать Запускаемый интерфейс, подобный этому:

@AllArgsConstructor
class GreedyWorker implements Runnable {
    private CountDownLatch threadsReadyCounter;
    private CountDownLatch threadsCalledBlocker;
    private CountDownLatch threadsCompletedCounter;

    @Override
    public void run() {
        long threadId = Thread.currentThread().getId();
        threadsReadyCounter.countDown();
        log.info("Thread-{} ready!", threadId);
        try {
            threadsCalledBlocker.await();
            try {
                ToiletPaper toiletPaper = toiletPaperService.grabToiletPaper();
                log.info("Thread-{} got Toilet Paper no. {}!", threadId, toiletPaper.getId());
            } catch (OutOfToiletPaperException ootpe) {
                log.info("No Toilet Paper in stock!");
            }
        } catch (InterruptedException ie) {
            log.info("Thread-{} got interrupted!", threadId);
        } finally {
            threadsCompletedCounter.countDown();
        }

    }
}

Вы видите, что мы определили три так называемых объекта CountDownLatch . Вы можете представить себе это как обратный отсчет. Вы можете определить число, например 3. Вызов функции await() заставляет поток ждать, пока обратный отсчет не достигнет 0. Итак, если бы мы вызвали обратный отсчет() три раза, выполнение все равно было бы приостановлено во время первых двух обратного отсчета() вызовов, но продолжилось бы, как только третий обратный отсчет() . Итак, что делает такой работник, так это:

  • обратный отсчет счетчик готовности потоков – сигнализирует о готовности к работе
  • подождите, пока потоки, вызываемые блокировщиком , не достигнут 0
  • возьмите туалетную бумагу и запишите, было ли это успешным
  • обратный отсчет счетчик завершенных потоков – сигнализирует о выполнении своей задачи

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

@Test
void multiThreadedGrabToiletPaper_NoLock() throws InterruptedException {
    CountDownLatch readyCounter = new CountDownLatch(NUMBER_OF_THREADS);
    CountDownLatch callBlocker = new CountDownLatch(1);
    CountDownLatch completeCounter = new CountDownLatch(NUMBER_OF_THREADS);

    List workers = Stream
            .generate(() -> new Thread(new GreedyWorker(readyCounter, callBlocker, completeCounter)))
            .limit(NUMBER_OF_THREADS)
            .collect(Collectors.toList());

    workers.forEach(Thread::start);

    readyCounter.await();
    log.info("Open the Toilet Paper Hunt!");
    callBlocker.countDown();
    completeCounter.await();
    log.info("Hunt ended!");
}

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

Теперь мы запускаем блокировщик вызовов, который, в свою очередь, заставляет каждого работника запускать свою логику захвата туалетной бумаги! И ждать, пока этот хаос не закончится. Вот что происходит:

5 Жадных работников – 3 туалетной бумаги в базе данных – Нет блокировки базы данных:

[           main] c.s.toiletpaperrush.ToiletPaperIT: Open the Toilet Paper Hunt!
[       Thread-3] c.s.toiletpaperrush.ToiletPaperIT: Thread-30 ready!
[       Thread-1] c.s.toiletpaperrush.ToiletPaperIT: Thread-28 ready!
[       Thread-5] c.s.toiletpaperrush.ToiletPaperIT: Thread-32 ready!
[       Thread-2] c.s.toiletpaperrush.ToiletPaperIT: Thread-29 ready!
[       Thread-4] c.s.toiletpaperrush.ToiletPaperIT: Thread-31 ready!
[       Thread-2] c.s.toiletpaperrush.ToiletPaperIT: Thread-29 got Toilet Paper no. 1!
[       Thread-3] c.s.toiletpaperrush.ToiletPaperIT: Thread-30 got Toilet Paper no. 1!
[       Thread-5] c.s.toiletpaperrush.ToiletPaperIT: Thread-32 got Toilet Paper no. 1!
[       Thread-4] c.s.toiletpaperrush.ToiletPaperIT: Thread-31 got Toilet Paper no. 1!
[       Thread-1] c.s.toiletpaperrush.ToiletPaperIT: Thread-28 got Toilet Paper no. 1!
[           main] c.s.toiletpaperrush.ToiletPaperIT: Hunt ended!

5 Работников-пациентов – 3 туалетной бумаги в базе данных – Блокировка базы данных:

[           main] c.s.toiletpaperrush.ToiletPaperIT: Open the Toilet Paper Hunt!
[       Thread-8] c.s.toiletpaperrush.ToiletPaperIT: Thread-35 ready!
[       Thread-6] c.s.toiletpaperrush.ToiletPaperIT: Thread-33 ready!
[       Thread-9] c.s.toiletpaperrush.ToiletPaperIT: Thread-36 ready!
[      Thread-10] c.s.toiletpaperrush.ToiletPaperIT: Thread-37 ready!
[       Thread-7] c.s.toiletpaperrush.ToiletPaperIT: Thread-34 ready!
[       Thread-8] c.s.toiletpaperrush.ToiletPaperIT: Thread-35 got Toilet Paper no. 4!
[      Thread-10] c.s.toiletpaperrush.ToiletPaperIT: Thread-37 got Toilet Paper no. 5!
[       Thread-9] c.s.toiletpaperrush.ToiletPaperIT: Thread-36 got Toilet Paper no. 6!
[       Thread-6] c.s.toiletpaperrush.ToiletPaperIT: No Toilet Paper in stock!
[       Thread-7] c.s.toiletpaperrush.ToiletPaperIT: No Toilet Paper in stock!
[           main] c.s.toiletpaperrush.ToiletPaperIT: Hunt ended!

Мы видим, что жадные ухватились за один и тот же рулон туалетной бумаги, хотя там еще оставались другие. Работники-пациенты – случай, когда только один поток мог получить доступ к одной туалетной бумаге одновременно, – хватали туалетную бумагу до тех пор, пока база данных не опустела. Но было обеспечено, чтобы никакие два потока не могли получить доступ к одному и тому же объекту одновременно.

Я надеюсь, что вы получили некоторое представление о том, как использовать Работоспособный s и Обратный отсчет es для написания многопоточного теста. Для всех заинтересованных, я был вдохновлен этим примером . И, может быть, вы вспомните об этом в следующий раз, когда отправитесь на охоту за мусором из туалетной бумаги в эти трудные времена! 😉

Оригинал: “https://dev.to/schmowser/how-to-write-multi-threaded-tests-in-java-2iki”