0%

Java死锁原理详解以及检测和解决死锁的方法

本文详细介绍了Java死锁的概念,构建、排查、以及解决办法,帮助程序员了解死锁。

死锁概述

两个或者多个线程互相持有对方所需要的资源(锁),都在等待对方执行完毕才能继续往下执行的时候,就称为发生了死锁。结果就是两个进程都陷入了无限的等待中。一般是有多个锁对象的情况下并且获得锁顺序不一致造成的。

比如两条线程分别使用两个锁A、B,线程一是获得锁顺序是A、B,线程二获得锁顺序是B、A。线程一获得锁A后释放cpu执行权。线程二获得锁B后释放cpu执行权。它们持有对方继续执行所需要的锁,也不能主动释放自己持有的锁,此时发生死锁!死锁的底层是基于锁的特性产生的,基本上锁只能同时被一个线程持有,后续线程将进入等待队列对待!。

死锁的产生的必要条件

死锁产生有四个必要条件,只要系统发生死锁则以上四个条件都必须成立。

  1. 互斥条件: 资源是独占的且排他使用,线程互斥使用资源,即任意时刻一个资源只能给一个线程使用,其他线程若申请一个资源,而该资源被另一线程占有时,则申请者等待直到资源被占有者释放。

  2. 不可剥夺条件: 线程所获得的资源在未使用完毕之前,不被其他线程强行剥夺,而只能由获得该资源的线程资源释放。

  3. 请求和保持条件: 线程每次申请它所需要的资源,在申请新的资源的同时,继续占用已分配到的资源。

  4. 循环等待条件: 在发生死锁时必然存在一个线程等待队列{P1,P2,…,Pn},其中P1等待P2占有的资源,P2等待P3占有的资源,…,Pn等待P1占有的资源,形成一个线程等待环路,环路中每一个线程所占有的资源同时被另一个申请,也就是前一个线程占有后一个线程所申请的资源。

事实上循环等待的成立蕴含了前三个条件的成立,似乎没有必要列出然而考虑这些条件对死锁的预防是有利的,因为可以通过破坏四个条件中的任何一个来预防死锁的发生。

构建死锁
下面构建了一个死锁样例,主要是由于获取资源(锁)的顺序不一致导致的。

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
public class DeadLock {
static ReentrantLock resource1 = new ReentrantLock();
static ReentrantLock resource2 = new ReentrantLock();

public static void main(String[] args) {
deadLock();

}

/**
* 构建两个线程,让他们发生死锁
*/
private static void deadLock() {
new Thread(() -> {
while (true) {
try {
resource1.lock();
System.out.println(Thread.currentThread().getName() + ": locked resource1");
try {
resource2.lock();
System.out.println(Thread.currentThread().getName() + ": locked resource2");
} finally {
resource2.unlock();
}
} finally {
resource1.unlock();
}
}
}, "deadLock-1").start();
new Thread(() -> {
while (true) {
try {
resource2.lock();
System.out.println(Thread.currentThread().getName() + ": locked resource2");
try {
resource1.lock();
System.out.println(Thread.currentThread().getName() + ": locked resource1");
} finally {
resource1.unlock();
}
} finally {
resource2.unlock();
}
}
}, "deadLock-2").start();
}

}

运行一段时间后可以发现,控制台不再输出。

死锁检测

遇到死锁问题的时候,我们很容易觉得莫名其妙,而且定位问题也很困难。一般发生死锁时,主要表现为相关线程不再工作,但是并没有抛出异常。

jstack

jstack命令用于生成虚拟机当前时刻的线程快照。

线程快照就是当前虚拟机内每一条线程正在执行的方法堆栈集合,生成线程快照的主要目的是定位线程出现长时间停顿的原因,如线程死锁、死循环、请求外部资源导致长时间等待等。jstack相比于jconsole更加的原始,没有图形化界面。

jstack 格式: jstack [option] vmid

控制台输入 jps 找到发生死锁的进程

控制台输入 jstack -l 108(108为死锁进程的pid)。出现线程堆栈信息,拉到最下面,即可发现死锁的信息。我们可以看到,这两个线程互相持有对方所需的资源,又互相需要对方的资源,因此发生了死锁。

如何预防和避免死锁

预防:
死锁的预防基本思想打破产生死锁的四个必要条件中的一个或几个,保证系统不会进入死锁状态。

比如:

  • 打破互斥条件:允许进程同时访问某些资源
  • 打破不剥夺条件:允许进程从占有者占有的资源中强行剥夺一些资源
  • 打破请求与保持条件:进程在运行前一次性地向系统申请它所需要的全部资源
  • 打破循环等待条件:实行资源有序分配策略

避免:

  • 加锁顺序(线程按照一定的顺序加锁)
  • 加锁时限(线程尝试获取锁的时候加上一定的时限,超过时限则放弃对该锁的请求,并释放自己占有的锁)
  • 死锁检测
    阿里巴巴中最新的开发规约,里面有对避免死锁的说明,具体如下:
    【强制】对多个资源、数据库表、对象同时加锁时,需要保持一致的加锁顺序,否则可能会造成死锁。 说明:线程一需要对表 A、B、C 依次全部加锁后才可以进行更新操作,那么线程二的加锁顺序也必须是 A、B、C,否则可能出现死锁。