Java 中什么情况会导致死锁?如何避免?

嗨,你好呀,我是猿java

在 Java编程中,死锁是一种常见的多线程问题,它发生在两个或多个线程彼此等待对方持有的资源时,导致这些线程都无法继续执行。死锁问题的解决和避免是多线程编程中的一个重要课题。这篇文章,我们一起来探讨 Java中死锁的情况及避免方法的详细。

1. 死锁的产生条件

死锁的发生通常需要满足以下四个条件:

  1. 互斥条件:资源不能被多个线程同时使用。即某个资源在某个时刻只能被一个线程占有。

  2. 占有且等待条件:一个线程已经持有至少一个资源,并且在等待获取额外的资源,而这些资源被其他线程持有。

  3. 不可剥夺条件:资源不能被强制剥夺,线程只能在完成任务后自愿释放所持有的资源。

  4. 环路等待条件:存在一个线程等待链,链中的每个线程都在等待链中的下一个线程所持有的资源。

当上面这四个条件同时满足时,就会发生死锁。

2. 死锁的案例

如下图:线程1持有 ResourceA的锁并等待 ResourceB的锁,线程2持有 ResourceB的锁并等待ResourceA的锁,这样Thread1和Thread2就形成了死锁。

img

下面,我们通过一个简单的Java示例来描述上面死锁的情况:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
public class DeadlockExample {

private static final Object lock1 = new Object();
private static final Object lock2 = new Object();

public static void main(String[] args) {
Thread thread1 = new Thread(() -> {
synchronized (lock1) {
System.out.println("Thread 1: Holding lock 1...");

try { Thread.sleep(10); } catch (InterruptedException e) {}

System.out.println("Thread 1: Waiting for lock 2...");
synchronized (lock2) {
System.out.println("Thread 1: Holding lock 1 & 2...");
}
}
});

Thread thread2 = new Thread(() -> {
synchronized (lock2) {
System.out.println("Thread 2: Holding lock 2...");

try { Thread.sleep(10); } catch (InterruptedException e) {}

System.out.println("Thread 2: Waiting for lock 1...");
synchronized (lock1) {
System.out.println("Thread 2: Holding lock 1 & 2...");
}
}
});

thread1.start();
thread2.start();
}
}

在这个例子中,thread1首先获得lock1,然后等待lock2,而thread2首先获得lock2,然后等待lock1。这就导致了死锁,因为两个线程都在等待对方持有的锁并且无法继续执行。

3. 避免死锁的方法

在 Java中,避免的死锁的方式还是比较丰富的,这里我列举了一些常见的避免死锁的方法:

3.1 资源排序法

资源排序法(Resource Ordering)是通过对资源进行全局排序,确保所有线程都按照相同的顺序获取锁,从而避免循环等待。例如,线程在获取多个锁时,总是先获取编号小的锁,再获取编号大的锁。

3.2 尝试锁

尝试锁(Try Lock)是使用tryLock()方法来代替lock()方法。在尝试获取锁时,设置一个超时时间,如果在规定时间内无法获得锁,则放弃获取锁,从而避免死锁。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
Lock lock1 = new ReentrantLock();
Lock lock2 = new ReentrantLock();

try {
if (lock1.tryLock(50, TimeUnit.MILLISECONDS)) {
try {
if (lock2.tryLock(50, TimeUnit.MILLISECONDS)) {
try {
// critical section
} finally {
lock2.unlock();
}
}
} finally {
lock1.unlock();
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}

3.3 超时放弃法

超时放弃法(Timeout and Retry)是指为线程等待资源的时间设置上限,如果超过这个时间还没有获得资源,则主动放弃,并稍后重试。

如下示例展示了超时放弃法的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.concurrent.TimeUnit;

public class TimeoutAvoidanceExample {

private static final Lock lock1 = new ReentrantLock();
private static final Lock lock2 = new ReentrantLock();

public static void main(String[] args) {
Thread thread1 = new Thread(new Task(lock1, lock2), "Thread-1");
Thread thread2 = new Thread(new Task(lock2, lock1), "Thread-2");

thread1.start();
thread2.start();
}

static class Task implements Runnable {
private final Lock firstLock;
private final Lock secondLock;

public Task(Lock firstLock, Lock secondLock) {
this.firstLock = firstLock;
this.secondLock = secondLock;
}

@Override
public void run() {
while (true) {
try {
// 尝试获取第一个锁
if (firstLock.tryLock(50, TimeUnit.MILLISECONDS)) {
try {
// 尝试获取第二个锁
if (secondLock.tryLock(50, TimeUnit.MILLISECONDS)) {
try {
// 成功获取两个锁后执行关键操作
System.out.println(Thread.currentThread().getName() + ": Acquired both locks, performing task.");
break; // 退出循环,任务完成
} finally {
secondLock.unlock();
}
}
} finally {
firstLock.unlock();
}
}
// 如果未能获取锁,则稍后重试
System.out.println(Thread.currentThread().getName() + ": Could not acquire both locks, retrying...");
Thread.sleep(10); // 等待一段时间后重试
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}

代码解读

  • 锁的定义:使用ReentrantLock来创建两个锁lock1和lock2。
  • 线程任务:Task类实现了Runnable接口,每个任务尝试以超时方式获取两个锁。
  • tryLock方法:tryLock(long time, TimeUnit unit)方法允许线程等待一段时间来获取锁,如果在指定时间内获取不到锁,则返回false。
  • 循环重试:如果线程未能在超时内获取到两个锁,它会释放已经获得的锁,等待一段时间后再次尝试。这种方式避免了死锁,因为线程不会无限期地等待锁。
  • 线程启动:创建并启动两个线程,每个线程尝试获取不同顺序的锁。

3.4 死锁检测

在某些情况下,可以使用死锁检测算法来发现死锁并采取措施。Java中的java.lang.management包提供了检测死锁的工具。

1
2
3
4
5
6
7
ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean();
long[] deadlockedThreads = threadMXBean.findDeadlockedThreads();

if (deadlockedThreads != null) {
System.out.println("Deadlock detected!");
// Handle the deadlock situation
}

3.5 减少锁的持有时间

尽量缩短锁的持有时间,确保在锁内执行的操作尽可能少,从而减少发生死锁的机会。

3.6 使用更高层次的并发工具

Java提供了许多高级并发工具类,如java.util.concurrent包下的ConcurrentHashMapSemaphoreCountDownLatch等,这些工具类在设计时就考虑了并发访问的安全性并减少了死锁的可能性。

3.7 避免嵌套锁

避免嵌套锁(Avoid Nested Locks,尽量避免一个线程在持有一个锁的同时去获取另一个锁,因为这会增加发生死锁的风险。

4. 总结

死锁是多线程编程中一个复杂而又让人头疼的问题,在实际开发中,死锁问题有时候发生还很难找到原因,因此,在日常开发中遵循良好的编程实践,可以有效地避免和处理死锁。

作为技术人员,需要掌握死锁产生根本原因,这样,即便死锁发生了也能快速的定位和解决。

5. 交流学习

最后,把猿哥的座右铭送给你:投资自己才是最大的财富。 如果你觉得文章有帮助,请帮忙转发给更多的好友,或关注公众号:猿java,持续输出硬核文章。

drawing