在经典的死锁示例中,代码中有两条路径获取相同的两个同步锁,但顺序不同,如下所示:
// 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 上的锁,甚至在尝试之前获取下一个锁。因此,使用 sleep 这个死锁模拟“不幸”的时机。比如说,有一个奇怪的要求,我们的代码应该有可能导致死锁,因此,我们想测试它:
// 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 类本身及其测试中。我必须这样做只是为了使测试始终通过。即使我从任何地方删除 sleep ,此代码有时可能会产生死锁,但不能保证它会在测试期间发生。现在说在这里 sleep 是 Not Acceptable ,那么问题是: 我如何可靠地测试此代码是否有可能在测试和实际代码本身中没有任何 sleep 的情况下导致死锁?
编辑:我只是想强调,我要求类有可能出现死锁,只有在某些“不幸”的时机(当两个线程调用 method1()
和 method2()
同时)这个死锁应该发生。在我的测试中,我想在每次运行时演示死锁。我想从生产代码中删除 sleep 调用(希望也从测试中删除)。也许有一种方法可以使用模拟而不是注入(inject)的监视器,这样我们就可以在测试期间安排它们以特定的顺序获取锁?
最佳答案
本质上,您需要执行method1
的Thread (t1)
在synchronized (monitor1)
内部等待,但在synchronized (monitor2)外部等待)
直到另一个执行 method2
的线程 (t2)
进入 synchronized (monitor2)
并释放 t1
并且两个线程都尝试继续。
反之亦然,t2
等待 t1
到来并释放
您可以自己编写此场景的代码。但由于您只关注死锁
测试,因此您可以在2方
之间使用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();
}
}
}
您必须处理由cycloBarrier.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());
关于java - 在 JUnit 中测试没有 sleep 的潜在死锁,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/62441456/