一不小心死锁了怎么办
死锁基础概念
什么是死锁
死锁是指两个或两个以上的线程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程。
简单来讲是指一组相互竞争资源的的线程因相互等待导致永久阻塞的现象。
示例代码
public class DeadLock {
public static void main(String[] args) {
Student student = new Student();
Student student1 = new Student();
Thread thread1 = new Thread(new SynAddRunnalbe(student, student1, true));
thread1.setName("Thread1");
Thread thread2 = new Thread(new SynAddRunnalbe(student, student1, false));
thread2.setName("thread2");
thread1.start();
thread2.start();
}
public static class SynAddRunnalbe implements Runnable {
Student student1, student2;
boolean flag;
public SynAddRunnalbe(Student student1, Student student2, boolean flag) {
this.student1 = student1;
this.student2 = student2;
this.flag = flag;
}
@Override
public void run() {
try {
if (flag) {
synchronized (student1) {
Thread.sleep(1000);
synchronized (student2) {
System.out.println("------------");
}
}
} else {
synchronized (student2) {
Thread.sleep(1000);
synchronized (student1) {
System.out.println("================");
}
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
class Student {
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
以上代码中需要关注flag的值,在SynAddRunnalbe类run()方法中通过flag使得不同的线程执行不同的代码。
在进程启动后,thread1执行的代码为if语句中的代码,先加锁student1,加锁成功后会加锁student2,thread2执行的代码为else语句中的代码,先加锁student2,加锁成功后再加锁student1,因为两个线程基本上会同时启动,thread1加锁student1成功后线程等待1s,这时一般情况下thread2也会将student2加锁成功,此时就会造成thread1等待thread2已经加锁成功的对象student2,而thread2等待thread1加锁成功的对象student1,并且这两个线程不会自动释放所持有的资源,此时就会造成死锁。
使用jdk工具(windows系统)排查死锁代码
使用jstack查看堆栈信息
1、查看进程号
使用jdk自带的bin文件夹下边的jps工具。使用jps -l命令
2、查看堆栈信息
使用jdk自带的bin文件夹下边的jstack工具。使用jstack 进程号命令
在堆栈的最后我们可以看到发生死锁的具体信息:
使用jconsole查看死锁信息
在jdk自带的bin文件夹下双击jconsole.exe,在打开的窗口中选择进程:
在界面中选择检查死锁
然后再新的界面中就可以看到死锁的具体信息
使用jvisualvm检查死锁
同样在bin目录下选择jvisualvm工具,选择对应线程:
然后我们在新展示出来的界面中看到于jstack一样的信息:
产生死锁的条件
互斥条件
同时存在两个或者两个以上的线程,并且存在多个共享资源且被不同的线程单独持有,例如共享资源x和y只能被一个线程所占用,不能被同一个线程同时占有。
占有且等待
线程t1在持有共享资源x,在等待共享资源y的时候不释放共享资源x。
不可抢占
其他线程不能抢占线程t1所持有的资源。
循环等待
线程t1等待线程t2持有的资源,线程t2等待线程t1持有的资源。
如何避免死锁
只要我们破坏掉死锁产生的四个条件中的某一个就可以避免死锁。
破环占有且等待条件
从理论上讲,要破坏这个条件,可以一次性申请所有资源。
破环不可抢占条件
破坏不可抢占条件可以使用如果申请不到共享资源就释放自己占有的资源,但是这一点synchronized是无法做到的,原因是synchronized在申请不到资源的时候线程是直接处于阻塞状态了,啥都干不了,也释放不了线程已经占有的资源,java.util.concurrent 这个包下面提供的 Lock 是可以轻松解决这个问题的。
破坏循环等待条件
破环循环等待条件可以对共享资源进行排序,并且按序进行申请资源,让每一个线程申请加锁的顺序都是一样的。
总结
在使用细粒度锁来锁定多个资源的时候,要注意死锁问题。预防死锁主要是破坏死锁的三个条件中的某一个,有时候避免死锁的成本是很高的,所以在选择具体的方案的时候还需要评估一下成本,选择其中成本最低的一个。