在经典的死锁示例中,代码中有两条路径获取相同的两个同步锁,但顺序不同,如下所示:
// 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 将获取监视器 1 上的锁,并且 method2 将获取监视器 2 上的锁,然后再尝试获取下一个锁。因此,使用睡眠这个死锁模拟“不幸”的时机。比如说,有一个奇怪的要求,我们的代码应该有可能导致死锁,因此,我们想测试它:
// 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 类本身及其测试中。我必须这样做只是为了使测试始终通过。即使我从任何地方删除睡眠,此代码有时可能会产生死锁,但不能保证它会在测试期间发生。现在说在这里睡觉是不可接受的,那么问题是: 我如何可靠地测试此代码是否有可能在测试和实际代码本身中没有任何睡眠的情况下导致死锁?
编辑:我只是想强调,我要求类有可能出现死锁,只有在某些“不幸”的时机(当两个线程同时调用
method1()
和 method2()
时)才会出现这种死锁应该发生。在我的测试中,我想在每次运行时演示死锁。我想从生产代码中删除睡眠调用(希望也从测试中删除)。也许有一种方法可以使用模拟而不是注入的监视器,这样我们就可以在测试期间安排它们以特定的顺序获取锁?
本质上,您需要
Thread (t1)
执行 method1
在 synchronized (monitor1)
内部但在 synchronized (monitor2)
外部等待,直到另一个执行 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());