Тестирование потенциального тупика без сна в JUnit - PullRequest
0 голосов
/ 18 июня 2020

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

// Production code
public class Deadlock {
    final private Object monitor1;
    final private Object monitor2;

    public Deadlock(Object monitor1, Object monitor2) {
        this.monitor1 = monitor1;
        this.monitor2 = monitor2;
    }

    public void method1() {
        synchronized (monitor1) {
            tryToSleep(1000);
            synchronized (monitor2) {
                tryToSleep(1000);
            }
        }
    }

    public void method2() {
        synchronized (monitor2) {
            tryToSleep(1000);
            synchronized (monitor1) {
                tryToSleep(1000);
            }
        }
    }

    public static void tryToSleep(long millis) {
        try {
            Thread.sleep(millis);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

Это потенциально может привести к взаимоблокировке. Чтобы увеличить вероятность того, что он действительно зайдет в тупик, я добавляю эти tryToSleep(1000);, просто чтобы гарантировать, что method1 получит блокировку на monitor1, а method2 получит блокировку на monitor2, прежде чем даже пытаться получить следующую блокировку. Таким образом, используя сон, этот тупик имитирует «неудачное» время. Скажем, есть странное требование, чтобы наш код мог привести к тупиковой ситуации, и по этой причине мы хотим его протестировать:

   // Test
   @Test
    void callingBothMethodsWillDeadlock() {
        var deadlock = new Deadlock(Integer.class, String.class);

        var t1 = new Thread(() -> {
            deadlock.method1(); // Executes for at least 1000ms
        });
        t1.start();

        var t2 = new Thread(() -> {
            deadlock.method2(); // Executes for at least 1000ms
        });
        t2.start();

        Deadlock.tryToSleep(5000); // We need to wait for 2s + 2s + some more to be sure...

        assertEquals(Thread.State.BLOCKED, t1.getState());
        assertTrue(t1.isAlive());

        assertEquals(Thread.State.BLOCKED, t2.getState());
        assertTrue(t2.isAlive());
    }

Успешно, и это хорошо. Что плохо, так это то, что мне пришлось добавить сон в сам класс Deadlock, а также в его тест. Пришлось сделать это только для того, чтобы тест стабильно проходил. Несмотря на то, что если я убираю сон отовсюду, этот код может иногда приводить к взаимоблокировке, но тогда нет гарантии, что это произойдет во время теста. Теперь предположим, что сон здесь неприемлем, тогда возникает вопрос: как я могу надежно проверить, что этот код может вызвать тупик без сна ни в тесте, ни в самом коде?

edit : Я просто хотел подчеркнуть, что я прошу класс иметь возможность зайти в тупик, только в некоторый «неудачный» момент (когда два потока вызывают method1() и method2() одновременно), этот тупик должен произойти . И в своем тесте я хочу продемонстрировать тупик при каждом запуске. Я хочу удалить вызовы сна из производственного кода (надеюсь, из теста). Может быть, есть способ использовать моки вместо внедренных мониторов, чтобы мы могли организовать их получение блокировок в определенном c порядке во время теста?

1 Ответ

0 голосов
/ 18 июня 2020

По сути, вам нужно Thread (t1) выполнение method1, чтобы ждать внутри synchronized (monitor1), но за пределами synchronized (monitor1), пока другой Thread (t2) выполнение method2 не войдет внутрь synchronized (monitor2) и не освободит t1, и оба потока попытаются продолжить.

Или наоборот, где t2 ждет, пока не придет t1 и не выпустит

. Вы можете написать этот сценарий самостоятельно. Но поскольку вы сосредоточены только на тестировании Deadlock, вы можете использовать java.util.concurrent.CyclicBarrier между 2 parties, чтобы организовать это, где parties указывает количество потоков, которые должны вызвать CyclicBarrier.await() перед срабатыванием барьера (в других случаях слов, все предыдущие потоки ожидают продолжения).

class Deadlock {
    final private CyclicBarrier cyclicBarrier = new CyclicBarrier(2);
    final private Object monitor1;
    final private Object monitor2;

    public Deadlock(Object monitor1, Object monitor2) {
        this.monitor1 = monitor1;
        this.monitor2 = monitor2;
    }

    public void method1() throws BrokenBarrierException, InterruptedException {
        synchronized (monitor1) {
            cyclicBarrier.await();
            synchronized (monitor2) {
            }
        }
    }

    public void method2() throws BrokenBarrierException, InterruptedException {
        synchronized (monitor2) {
            cyclicBarrier.await();
            synchronized (monitor1) {
            }
        }
    }

    public static void tryToSleep(long millis) {
        try {
            Thread.sleep(millis);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

Вам нужно будет обработать отмеченные исключения, сгенерированные cyclicBarrier.await()

        Thread t1 = new Thread(() -> {
            try {
                deadlock.method1(); 
            } catch (BrokenBarrierException | InterruptedException e) {
                e.printStackTrace();
            }
        });
        t1.start();

        Thread t2 = new Thread(() -> {
            try {
                deadlock.method2();
            } catch (BrokenBarrierException | InterruptedException e) {
                e.printStackTrace();
            }
        });
        t2.start();

        deadlock.tryToSleep(5000); // Wait till all threads have a chance to become alive

        assertEquals(Thread.State.BLOCKED, t1.getState());
        assertTrue(t1.isAlive());

        assertEquals(Thread.State.BLOCKED, t2.getState());
        assertTrue(t2.isAlive());
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...