1.什么是死锁
在Java中,死锁是指两个或多个线程互相等待对方释放资源或锁定其持有的资源而导致的一种僵局。一个线程正在等待其它线程释放它需要的资源,而被它需要的资源已被其它线程占用,这样的情况称为死锁。死锁导致的结果是线程被永久地挂起,无法继续执行。
举个例子,假设有两个线程,A和B,A需要获取锁1和锁2,B需要获取锁2和锁1。如果A获取了锁1,但是无法获取锁2,B获取了锁2,但是无法获取锁1,那么就形成了死锁。
public class DeadlockDemo {
private static Object lock1 = new Object();
private static Object lock2 = new Object();
public static void main(String[] args) {
new Thread(() -> {
synchronized (lock1) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (lock2) {
System.out.println("Thread1 executed successfully.");
}
}
}).start();
new Thread(() -> {
synchronized (lock2) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (lock1) {
System.out.println("Thread2 executed successfully.");
}
}
}).start();
}
}
在上面的代码中,Thread1需要先获取锁1然后获取锁2,而Thread2需要先获取锁2然后获取锁1,当两个线程都启动后,它们彼此等待并且不会释放锁,导致死锁的发生。
2.如何避免死锁
2.1.避免嵌套锁
避免嵌套锁的一种方法是使用tryLock()方法而不是synchronized关键字,tryLock()方法会尝试获取锁并返回其结果,而不是让线程一直等待。 另外,可以使用synchronized关键字来保护类级别的共享资源,以避免嵌套锁。
public class DeadlockDemo {
private static Object lock1 = new Object();
private static Object lock2 = new Object();
public static void main(String[] args) {
new Thread(() -> {
if (tryLock(lock1)) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
if (tryLock(lock2)) {
System.out.println("Thread1 executed successfully.");
unlock(lock2);
}
unlock(lock1);
}
}).start();
new Thread(() -> {
if (tryLock(lock2)) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
if (tryLock(lock1)) {
System.out.println("Thread2 executed successfully.");
unlock(lock1);
}
unlock(lock2);
}
}).start();
}
public static boolean tryLock(Object lock) {
return LockSupport.tryAcquire(lock) ? true : false;
}
public static void unlock(Object lock) {
LockSupport.unpark((Thread) lock);
}
}
上面的代码中,使用了tryLock()方法来获取锁,而不是直接使用synchronized关键字。注意,tryLock()方法返回Boolean值,当尝试获取锁成功时返回true,否则返回false。如果一个线程不能获取到某个锁,那么它会释放已经获得的资源并且让其他线程尝试获取锁。这样可以有效的避免死锁。
2.2.按照相同的顺序请求锁
另一种方式是让线程按照相同的顺序请求锁。 假设有两个线程A和B,A需要获取锁1和锁2,B需要获取锁2和锁1。如果A和B都按照先请求锁1然后请求锁2的顺序来获取锁,那么就可以避免死锁的发生。
public class DeadlockDemo {
private static Object lock1 = new Object();
private static Object lock2 = new Object();
public static void main(String[] args) {
new Thread(() -> {
synchronized (lock1) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (lock2) {
System.out.println("Thread1 executed successfully.");
}
}
}).start();
new Thread(() -> {
synchronized (lock1) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (lock2) {
System.out.println("Thread2 executed successfully.");
}
}
}).start();
}
}
在上面的代码中,Thread1和Thread2都首先获取锁1,然后再获取锁2。这样做可以避免死锁的发生。
2.3.设置超时时间
还可以在获取锁的时候设置超时时间。 如果一个线程在指定的时间内没有获取到锁,则它会释放所有已获得的锁并退出。这样可以避免死锁。
public class DeadlockDemo {
private static Object lock1 = new Object();
private static Object lock2 = new Object();
public static void main(String[] args) {
new Thread(() -> {
try {
if (lock1()) {
System.out.println("Thread1 got lock1.");
Thread.sleep(1000);
if (lock2()) {
System.out.println("Thread1 got lock2.");
unlock(lock2);
}
unlock(lock1);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
new Thread(() -> {
try {
if (lock2()) {
System.out.println("Thread2 got lock2.");
Thread.sleep(1000);
if (lock1()) {
System.out.println("Thread2 got lock1.");
unlock(lock1);
}
unlock(lock2);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
}
public static boolean lock1() throws InterruptedException {
return LockSupport.parkNanos(lock1, TimeUnit.SECONDS.toNanos(2)) && true;
}
public static boolean lock2() throws InterruptedException {
return LockSupport.parkNanos(lock2, TimeUnit.SECONDS.toNanos(2)) && true;
}
public static void unlock(Object lock) {
LockSupport.unpark((Thread) lock);
}
}
在上面的代码中,lock1()方法和lock2()方法都会等待2秒钟来获取锁。如果在等待时间内获取锁失败,则方法会返回false。每个线程都在获取锁的同时休眠1秒钟来模拟繁重的任务。如果一个线程获取到了锁但是在完成任务后没有释放锁,另一个线程仍然可以在指定的时间内获取到它所需要的锁,并继续执行。
3.补充说明
死锁是多线程编程中常见的问题之一,对于Java开发者来说尤为重要。上面我们介绍了一些避免死锁的方法,但实践中可能存在其他的方法,在使用多线程的过程中我们需要注意这个问题并及时解决。当然,避免死锁的同时也需要保证线程安全和性能。