死锁应该可以说是并发编程中比较常见的一种情况,可以说如果程序产生了死锁那将会对程序带来致命的影响;所以排查定位、修复死锁至关重要;
我们都知道死锁是由于多个对象或多个线程之间相互需要对方锁持有的锁而又没有释放对方所持有的锁,导致双方都永久处于阻塞状态;
如上图所示,线程1持有对象1的锁、线程2持有对象2的锁,持此线程1又想去获取对象2对象锁、线程2想获取对象1对象锁,此时由于双方都没有获取到想要的锁,任务没完成所以也没释放锁,导致一直僵持呢,于是阻塞、产生死锁;
死锁检测
需要检测死锁肯定要先有死锁出现,下面的demo模拟了一个死锁的产生;
public class DeadlockDemo extends Thread {
private BaseObj first;
private BaseObj second;
public DeadlockDemo(String name, BaseObj first, BaseObj second) {
super(name);
this.first = first;
this.second = second;
}
public void reentrantLock() throws InterruptedException {
first.lock();
System.out.println(String.format("%s 持有:%s 对象锁,等待获取:%s对象锁", this.getName(), first, second));
second.lock();
first.unlock();
second.unlock();
}
@Override
public void run() {
try {
reentrantLock();
} catch (Exception e) {
e.printStackTrace();
}
}
public static void main(String[] args) throws InterruptedException {
ObjOne one = new ObjOne();
ObjTwo two = new ObjTwo();
DeadlockDemo thread1 = new DeadlockDemo("Thread1", one, two);
DeadlockDemo thread2 = new DeadlockDemo("Thread2", two, one);
thread1.start();
thread2.start();
thread1.join();
thread2.join();
}
}
运行上面的demo将看到程序被阻塞了,没法结束运行;只看到如下运行结果:
Thread1 持有:objOne 对象锁,等待获取:objTwo对象锁 Thread2 持有:objTwo 对象锁,等待获取:objOne对象锁
这demo没法结束运行就是由于产生了死锁,两个线程都在相互对待获取对方所持有的对象锁;
这时候要解决问题就需要找出哪里出现了死锁,通过代码走查通常不容易发现死锁,当然我们这程序很容易发现,因为我们刻意产生的死锁;所以就需要工具来检测死锁,这里可用的工具主要有:jconsole、jvisualvm、jstack等,这些工具其实都是jdk自带的,用法都很类似;
这里使用jvisualvm来检测当前的demo程序是否产生了死锁;打开jvisualvm连接到当前的应用程序即可看到程序的监控信息,如内存、CPU、性能、GC等等;打开进入线程的tab项查看程序的线程信息,这里很明显的就看到了提示该程序被检测除了死锁!
点击 线程Dump可以看到线程的堆栈信息,从中可以看到线程的详细信息,并定位死锁;
从上图可以看到线程产生死锁的原因,Thrad2是等待Thread1、Thread1是等待Thread1, 从下图的堆栈信息即可定位死锁产生的位置;
死锁扫描
除了发现程序出现问题后我们去扫描死锁外,我们还可以实时的去扫描程序用于发现程序中是否存在死锁;
JDK提供了MXBean Api可用于扫描程序是否存在死锁,ThreadMXBean提供了findDeadlockedThreads()方法,可以用于找到产生死锁的线程;这里在上面的demo程序中添加一个方法用于扫描死锁,虽然这种方法可以扫描到死锁但是由于每次都对线程打快照对程序性能会有比较大的影响,所以慎用;
public static void scanDeadLock() {
ThreadMXBean mxBean = ManagementFactory.getThreadMXBean();
Runnable runnable = () -> {
long[] ids = mxBean.findDeadlockedThreads();
System.out.println("扫描死锁...");
if (ids != null) {
ThreadInfo[] threadInfos = mxBean.getThreadInfo(ids);
for (ThreadInfo threadInfo : threadInfos) {
System.out.println(threadInfo);
}
}
};
ScheduledExecutorService executorService = new ScheduledThreadPoolExecutor(5, Executors.defaultThreadFactory());
executorService.scheduleAtFixedRate(runnable, 1, 5, TimeUnit.SECONDS);
}
避免死锁
解决死锁最好的方法就是避免死锁了,比如上面的demo我们可以把直接使用无参数的lock()方法换为使用tryLock方法,tryLock还可以指定获取锁超时时间,到了超时时间还没获得到锁就会放弃获取锁,当然还有其它方法可以避免死锁;
1、避免使用多个锁、长时间持有锁;
2、设计好多个锁的获取顺序
3、使用带超时的获取锁方法