1. 什么是死锁?
可以这样理解,两个或两个以上的线程在执行过程中,因竞争资源而造成一种相互等待的现象,若无外力作用,它们都将无法向前推进。
2. 产生死锁的4个必要条件
- 互斥条件
进程要求对所分配的资源(如打印机)进行排他性控制,即在一段时间内某 资源仅为一个进程所占有。此时若有其他进程请求该资源,则请求进程只能等待
- 不剥夺条件
进程所获得的资源在未使用完毕之前,不能被其他进程强行夺走,即只能 由获得该资源的进程自己来释放(只能是主动释放)。
- 请求和保持条件
进程已经保持了至少一个资源,但又提出了新的资源请求,而该资源 已被其他进程占有,此时请求进程被阻塞,但对自己已获得的资源保持不放。
- 循环等待条件
存在一种进程资源的循环等待链,链中每一个进程已获得的资源同时被 链中下一个进程所请求。
四个条件是死锁的必要条件,只要系统发生死锁,这些条件必然成立,而只要上述条件之一不满足,就不会发生死锁。
死锁的四个条件 :1、互斥 2、保持锁并请求锁 3、不可抢夺 4、循环等待
3. 死锁常见案例
(1)synchronized 死锁
线程A获得了锁1,线程B获得了锁2,这时线程A又需要获得锁2,线程B需要获得锁1。 这种情况下就会出现死锁,因为如果没有外力破坏的话,线程A永远获得不了锁2,线程B也永远获得不了锁1,就会永远的相互等待。
package priv.allen.javathread.deadlock;
/**
* dead lock demo
* @author Allen
*
*/
public class DeadLockDemo {
private static Object lock1 = new Object();
private static Object lock2 = new Object();
/**
* @param args
*/
public static void main(String[] args) {
deadLock();
}
private static void deadLock() {
Thread tA = new Thread(() -> {
synchronized(lock1) {
try {
System.out.println(Thread.currentThread().getName() + " has got lock1"); //$NON-NLS-1$
Thread.sleep(500);
System.out.println(Thread.currentThread().getName() + " already sleeped 500ms"); //$NON-NLS-1$
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " is going to get lock2"); //$NON-NLS-1$
synchronized (lock2) {
System.out.println(Thread.currentThread().getName() + " has got lock2"); //$NON-NLS-1$
}
}
}, "thread A"); //$NON-NLS-1$
Thread tB = new Thread(() -> {
synchronized(lock2) {
try {
System.out.println(Thread.currentThread().getName() + " has got lock2"); //$NON-NLS-1$
Thread.sleep(500);
System.out.println(Thread.currentThread().getName() + " already sleeped 500ms"); //$NON-NLS-1$
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " is going to get lock1"); //$NON-NLS-1$
synchronized (lock1) {
System.out.println(Thread.currentThread().getName() + " has got lock1"); //$NON-NLS-1$
}
}
}, "thread B"); //$NON-NLS-1$
tA.start();
tB.start();
}
}
执行结果如下:
thread B has got lock2
thread A has got lock1
thread B already sleeped 500ms
thread A already sleeped 500ms
thread A is going to get lock2
thread B is going to get lock1
由此可见,线程A,B都只执行到了获取对方的锁,就一直在等待了。
(2) Lock 的错误用法会导致死锁
package priv.allen.javathread.deadlock;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* lock.unlock();释放锁使用地方不规范,导致死锁不能正常释放!
* lock.unlock() 建议在 finally 中释放
* @author Allen
*
*/
public class LockDeadLockDemo {
public static void main(String[] args) {
final DeadLockBean deadLockBean = new DeadLockBean();
Thread a = new Thread(() -> {
try {
Thread.sleep(300);
} catch (InterruptedException e) {
e.printStackTrace();
}
try {
deadLockBean.productDeadLock();
} catch (Throwable throwable) {
throwable.printStackTrace();
}
}, "thread A");
Thread b = new Thread(() -> {
try {
Thread.sleep(300);
} catch (InterruptedException e) {
e.printStackTrace();
}
try {
deadLockBean.productDeadLock();
} catch (Throwable throwable) {
throwable.printStackTrace();
}
}, "thread B");
a.start();
b.start();
}
public static class DeadLockBean{
private Lock lock = new ReentrantLock();
public void productDeadLock() throws Throwable {
System.out.println(Thread.currentThread().getName() + " 进入了方法!");
lock.lock();
try{
System.out.println(Thread.currentThread().getName() + " 已经执行了!");
throw new Throwable("人为抛出异常Throwable"); //关键代码行1,
//throw new Exception("人为抛出异常Exception");//关键代码行2,不会死锁,会在catch(Exception e中被捕获),嵌套lock.unlock()并释放
}catch(Exception e){
// 这里只能catch 到 exception, catch不到throwable
System.out.println(Thread.currentThread().getName()+" 发生异常catch!");
//lock.unlock();//关键代码行3,不建议在这里释放,假如发生【关键代码行1】会产生死锁
}finally{
System.out.println(Thread.currentThread().getName()+" 发生异常finally!");
lock.unlock();//关键代码行4,无论发生何种异常,均会释放锁。
}
//lock.unlock();//关键代码行5,假如发生不能捕获异常,将跳出方法体,不执行此处
System.out.println(Thread.currentThread().getName() + " tryCatch外释放锁!");
}
}
}
Lock 的 unlock() 方法建议在finally中释放,否则容易出现死锁。
4. 怎么避免死锁?
一旦我们在一个同步方法中调用了其他对象的延时方法或同步方法,就要十分的小心:
- 如果其他对象的这个方法会消耗比较长的时间,那么就会导致锁被持有很长的时间;
- 如果其他对象的这个方法也是一个同步方法,那么就要注意避免发生死锁的可能性。
最好是能够避免在一个同步方法中调用其他对象的同步方法和延时方法。
尽量避免使用静态同步方法,因为静态同步想当于类锁(全局锁)。
如果不能避免,就要采取一些编码技巧,来打破锁的闭环。去下面所示:
public class ClassB {
private String address;
// ...
public synchronized void method1(){
// do something
}
// ... ...
}
public class ClassA {
private int id;
private String name;
private ClassB b;
// ...
public synchronized void m1(){
// do something
b.method1();
}
// ... ...
}
上面的ClassA.m1()方法,在对象的同步方法中又调用了ClassB的同步方法method1(),所以存在死锁发生的可能性。我们可以修改如下,避免死锁:
public class ClassA {
private int id;
private String name;
private ClassB b;
// ...
public void m2(){ // 方法并没有使用同步,普通方法
synchronized(this){ // 方法内使用了同步块
// do something
}
b.method1(); // B中的同步方法,在A的普通方法中调用
}
// ... ...
}
这样的话减小了锁定的范围,两个锁的申请就没有发生交叉,避免了死锁的可能性,这是最理性的情况,因为锁没有发生交叉。