在 JUnit 中不休眠测试潜在的死锁

Testing a potential deadlock without sleep in JUnit

在classical死锁示例中,代码中有两条路径获取相同的两个同步锁,但顺序不同,如下所示:

// 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);,只是为了确保方法 1 将获取监视器 1 上的锁,并且方法 2 将在尝试获取下一个锁之前获取监视器 2 上的锁。所以使用睡眠这个死锁模拟 "unlucky" 计时。说,有一个奇怪的要求,我们的代码应该有可能导致死锁,因此,我们要测试它:

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

这就过去了,很好。不好的是我不得不将 sleep 添加到 Deadlock class 本身以及它的测试中。我必须这样做只是为了让测试始终如一地通过。即使我从所有地方删除睡眠,这段代码有时也会产生死锁,但不能保证它会在测试期间发生。现在在这里说睡觉是不可接受的,那么问题是: 我如何才能可靠地测试此代码是否有可能在测试和实际代码本身中都没有任何休眠地导致死锁?

编辑:我只是想强调,我要求 class 有可能出现死锁,仅在某些 "unlucky" 时间(当两个线程正在调用 method1()method2() 同时发生)这个死锁应该发生。在我的测试中,我想在每个 运行 上演示死锁。我想从生产代码中删除睡眠调用(希望也从测试中删除)。也许有一种方法可以使用模拟而不是注入监视器,这样我们就可以在测试期间安排它们以特定顺序获取锁?

本质上,您需要 Thread (t1) 执行 method1 以在 synchronized (monitor1) 内部但在 synchronized (monitor1) 外部等待,直到另一个 Thread (t2) 执行 method2 执行在 synchronized (monitor2) 内并释放 t1 并且两个线程都尝试继续。

反之亦然,t2 等到 t1 出现并释放

你可以自己编写这个场景。但是因为你只关注 Deadlock 测试,你可以在 2 parties 之间使用 java.util.concurrent.CyclicBarrier 来协调这个,其中 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());