3、死锁
死锁是指在并发系统中,两个或多个线程或进程互相等待对方持有的资源,从而导致这些线程或进程永久阻塞的状态。简单来说,死锁是系统中一组进程因相互持有对方所需的资源而陷入永久等待的情况。死锁是并发编程中的一个常见问题,会导致程序无法继续执行,需要避免和处理。
3.1 死锁的四个必要条件
要发生死锁,必须同时满足以下四个条件:
**1)互斥条件:**指线程或进程对所分配到的资源进行排它性使用,即在一段时间内某资源只由一个进程占用。如果此时还有其它进程请求资源,则请求者只能等待,直至占有资源的进程用毕释放。
**2)请求和保持条件:**指线程或进程已经保持至少一个资源,但又提出了新的资源请求,而该资源已被其它进程占有,此时请求进程阻塞,但又对自己已获得的其它资源保持不放。
**3)不剥夺条件:**指线程或进程已获得的资源,在未使用完之前,不能被剥夺,只能在使用完时由自己释放。
**4)环路等待条件:**指在发生死锁时,必然存在一个线程或进程——资源的环形链,即进程集合{P0,P1,P2,···,Pn}中的P0正在等待一个P1占用的资源;P1正在等待P2占用的资源,……,Pn正在等待已被P0占用的资源。
3.2 Java 中死锁示例
下面是一个简单的 Java 示例,演示了两个线程如何由于相互等待对方持有的资源而导致死锁。
public class DeadLockDemo {
private static String A = "A";
private static String B = "B";
public static void main(String[] args){
new DeadLockDemo().deadLock();
}
private void deadLock() {
Thread t1 = new Thread(new Runnable() {
public void run() {
synchronized(A){
try {
Thread.currentThread().sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized(B){
System.out.println("1");
}
}
}
});
Thread t2 = new Thread(new Runnable() {
public void run() {
synchronized(B){
synchronized(A){
System.out.println("2");
}
}
}
});
t1.start();
t2.start();
}
}
3.2.1 解释
- 共享资源:
private static String A = "A";
private static String B = "B";
- 这两个字符串对象
A
和B
是共享资源,两个线程都会尝试获取这些资源的锁。
- 主方法:
public static void main(String[] args)
启动程序,并调用new DeadLockDemo().deadLock()
创建并启动两个线程。
- 创建死锁的方法:
private void deadLock()
方法中创建了两个线程t1
和t2
,这两个线程会导致死锁。
- 线程1 (t1):
- 线程
t1
首先获取A
的锁,持有锁 2 秒钟(使用Thread.sleep(2000)
)。 - 在持有
A
的锁的情况下,尝试获取B
的锁。
- 线程
- 线程2 (t2):
- 线程
t2
首先获取B
的锁。 - 在持有
B
的锁的情况下,尝试获取A
的锁。
- 线程
3.2.2 死锁发生的原因
- 线程1 锁定
A
并休眠 2 秒钟。 - 线程2 锁定
B
。 - 线程1 醒来后,尝试锁定
B
,但此时B
已被线程2
锁定,因此线程1
被阻塞等待B
。 - 线程2 尝试锁定
A
,但此时A
已被线程1
锁定,因此线程2
被阻塞等待A
。 - 结果:两个线程相互等待对方释放资源,从而导致死锁。
3.2.3 dump 线程排查问题
通过dump 线程排查哪个线程出了问题。下面可以看到 DeadLockDemo.java的29行和 DeadLockDemo.java 的39行这两个位置引起了死锁,线程Thread-0 和 Thread-1是 的状态是BLOCKED-阻塞。
"Thread-0" #12 prio=5 os_prio=0 cpu=4.11ms elapsed=231.92s tid=0x00007f9b440d8bb0 nid=0x5655 waiting for monitor entry [0x00007f9b2c7f9000]
java.lang.Thread.State: BLOCKED (on object monitor)
at DeadLockDemo$1.run(DeadLockDemo.java:29)
- waiting to lock <0x00000000fff00f28> (a java.lang.String)
- locked <0x00000000c9d18d68> (a java.lang.String)
at java.lang.Thread.run(java.base@17.0.1/Thread.java:833)
"Thread-1" #13 prio=5 os_prio=0 cpu=4.24ms elapsed=231.92s tid=0x00007f9b440d9980 nid=0x5656 waiting for monitor entry [0x00007f9b2c6f8000]
java.lang.Thread.State: BLOCKED (on object monitor)
at DeadLockDemo$2.run(DeadLockDemo.java:39)
- waiting to lock <0x00000000c9d18d68> (a java.lang.String)
- locked <0x00000000fff00f28> (a java.lang.String)
at java.lang.Thread.run(java.base@17.0.1/Thread.java:833)
3.3 如何避免死锁
-
确保所有线程按相同的顺序获取资源。这可以避免循环等待的情况。
示例:
java复制代码private void deadLockAvoided() { Thread t1 = new Thread(new Runnable() { public void run() { synchronized(A){ synchronized(B){ System.out.println("1"); } } } }); Thread t2 = new Thread(new Runnable() { public void run() { synchronized(A){ synchronized(B){ System.out.println("2"); } } } }); t1.start(); t2.start(); }
-
使用
tryLock
等方法设置获取锁的超时时间,避免线程无限期等待。 -
避免一个线程同时获取多个锁。
-
避免一个线程在锁内同时占用多个资源,尽量保证每个锁只占用一个资源。
-
对于数据库锁,加锁和解锁必须在同一个数据库连接里,否则会出现解锁失败的情况。