死锁的定义
在多线程环境下,当两个或多个线程都在等待对方执行完毕后才开始执行自己的代码时,就会发生死锁。这演变成了一种互相等待的情况,在这种情况下,没有任何线程能继续执行。 死锁是非常危险的程序行为,通常需要重新启动应用程序才能解决,因此必须避免发生死锁的情况。
死锁的例子
下面是一个简单的示例,其中两个线程试图获取彼此持有的锁,然后相互等待的情况。
public class DeadLockDemo {
private final Object lock1 = new Object();
private final Object lock2 = new Object();
public void execute() {
Thread t1 = new Thread(() -> {
synchronized (lock1) {
System.out.println("Thread 1 acquired lock1");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (lock2) {
System.out.println("Thread 1 acquired lock2");
}
}
});
Thread t2 = new Thread(() -> {
synchronized (lock2) {
System.out.println("Thread 2 acquired lock2");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (lock1) {
System.out.println("Thread 2 acquired lock1");
}
}
});
t1.start();
t2.start();
}
}
在这个示例代码中,线程1获取lock1锁并等待1秒钟后,试图获取lock2锁;线程2获取lock2锁并等待1秒钟后,试图获取lock1锁。由于它们都试图获取彼此持有的锁,所以这两个线程都被阻塞。
解决死锁的方案
1. 避免嵌套锁
一旦有线程持有了一个锁,其他线程就不能访问这个锁。因此,如果两个线程都在等待对方释放自己持有的锁,那么他们将永远无法运行。为了避免死锁,最好避免嵌套锁。
public class DeadLockDemo1 {
private final Object lock1 = new Object();
private final Object lock2 = new Object();
public void execute() {
Thread t1 = new Thread(() -> {
synchronized (lock1) {
System.out.println("Thread 1 acquired lock1");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
Thread t2 = new Thread(() -> {
synchronized (lock2) {
System.out.println("Thread 2 acquired lock2");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t1.start();
t2.start();
}
}
在示例代码中,线程1获取lock1锁,但仅仅释放该锁,不试图获取lock2锁。线程2也是一样。因此这两个线程将不会发生死锁。
2. 避免循环等待
另一个避免死锁的方法是避免循环等待。为了达到这个目的,可以对所有锁进行排序,并按照相同的顺序来获得它们。这可以确保死锁不会发生。
public class DeadLockDemo2 {
private final Object lock1 = new Object();
private final Object lock2 = new Object();
public void execute() {
Thread t1 = new Thread(() -> {
synchronized (lock1) {
System.out.println("Thread 1 acquired lock1");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (lock2) {
System.out.println("Thread 1 acquired lock2");
}
}
});
Thread t2 = new Thread(() -> {
synchronized (lock1) {
System.out.println("Thread 2 acquired lock1");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (lock2) {
System.out.println("Thread 2 acquired lock2");
}
}
});
t1.start();
t2.start();
}
}
在这个示例代码中,线程1获取lock1锁之后,获得lock2锁。线程2获取lock1锁前,线程1必须已经释放了lock1锁。
3. 使用超时
在一些特定情况下,死锁是难以避免的。在这种情况下,可以使用超时策略来避免死锁。当获取一个锁时,可以设置一个超时时间,然后在超过指定时间后抛出异常。
public class DeadLockDemo3 {
private final Object lock1 = new Object();
private final Object lock2 = new Object();
public void execute() {
Thread t1 = new Thread(() -> {
try {
synchronized (lock1) {
System.out.println("Thread 1 acquired lock1");
Thread.sleep(1000);
synchronized (lock2) {
System.out.println("Thread 1 acquired lock2");
}
}
} catch (InterruptedException e) {
throw new IllegalStateException(e);
}
});
Thread t2 = new Thread(() -> {
try {
synchronized (lock1) {
System.out.println("Thread 2 acquired lock1");
Thread.sleep(1000);
synchronized (lock2) {
System.out.println("Thread 2 acquired lock2");
}
}
} catch (InterruptedException e) {
throw new IllegalStateException(e);
}
});
t1.start();
t2.start();
}
}
在示例代码中,线程1和线程2都试图获取lock1锁,然后等待1秒钟,然后试图获取lock2锁。由于这些锁有可能处于循环等待状态,因此这里使用了锁超时策略。
总结
死锁是程序中一种不可避免的情况,但是可以通过使用上述的避免方案来减少它们的发生。最好的方法是避免嵌套锁和循环等待,如果这些避免方法不适用,那么超时策略也可以实现死锁的防治。