一、什么是死锁
多个进程竞争有限数量的资源时,当一个进程申请的资源被其他等待进程占用,那么该等待线程有可能再也无法改变状态,这种情况被称为死锁,一旦程序发生死锁,线程就会崩溃,无法执行后续工作。
二、死锁的三个典型情况
1.情况一
一个线程,使用一把锁,进行连续加锁两次,如果该锁是不可重入锁,就会死锁
2.情况二
两个线程t1,t2,使用两把锁,t1,t2先对进行自己加锁,再尝试获取对方
举个例子:现在有t1和t2两个人,在吃面条,都想要加辣椒和醋,其中t1拿到了辣椒,t2拿到了醋,t1对t2说你把醋给我,我用完了给你,t2对t1说你把辣椒给我,我用完了给你,如果两人均不想后退一步,那么就会僵持,就会出现死锁状态
Object lajiao = new Object();
Object cu = new Object();
Thread t1 = new Thread(() -> {
synchronized (lajiao) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (cu) {
System.out.println("辣椒和醋都拿到了");
}
}
});
Thread t2 = new Thread(() -> {
synchronized (cu) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (lajiao) {
System.out.println("辣椒和醋都拿到了");
}
}
});
t1.start();
t2.start();
3.情况三
多个线程,使用多把锁,相当于情况二的一般情况
经典例子:哲学家就餐问题是一个经典的并发控制问题,它描述了一组哲学家围坐在一个圆桌旁,每个人都有一双筷子,他们需要同时拿起左右两边的筷子才能吃饭。
这个问题可以抽象为以下五个条件:
1.每个哲学家同时拿起左右两边的筷子;
2.如果一个哲学家左右两边的筷子都已经被其他哲学家拿走,那么这个哲学家就会等待;
3.当一个哲学家拿到左右两边的筷子后,他就会开始吃饭;
4.吃完饭后,哲学家会放下手中的筷子;
5.哲学家吃饭和思考的过程是交替进行的。
在这个问题中,可能会出现死锁的情况。当所有的哲学家同时拿起左边的筷子时,他们都在等待右边的筷子被释放。然而,由于没有哲学家释放右边的筷子,所以所有的哲学家都会陷入无限等待的状态,从而产生死锁。
三、可重入和不可重入
在情况一中我们说到不可重入锁的问题,现在我们就来说明,什么是可重入锁和不可重入锁。
简单而言,一个线程针对同一个对象,连续加锁两次,是否会有问题,如果没问题,就叫可重入,如果有问题,就叫不可重入
public int count = 0;
synchronized public void add() {
synchronized (this) {
count++;
}
}
在上图代码中,锁的对象是this,如果有线程调用add()方法,就会先对add()方法进行加锁操作,然后遇到了代码块,再次尝试加锁操作,当站在锁对象的角度来说,自己已经加锁了,被其他线程给占用,现这里的第二次加锁是否需要阻塞等待,但是这里特殊的是,这里的两次加锁都为同一个线程所为,如果允许上述操作,不阻塞等待,那么就是可重入锁,如果不允许上述操作,阻塞等待就是不可重入锁,并且该线程就会死锁
在Java中synchronized就是可重入锁,当遇到两次加锁时,会在锁对象中检查,当加锁线程和持有锁线程是同一个线程时,就会直接通过,否则进行阻塞
四、死锁的四个必要条件
1.互斥使用
当线程1拿到对象并加锁后,线程2必须等待
2.不可抢占
线程1拿到对象并加锁后,必须是线程1主动释放锁,线程2才能获取资源,线程2不能将对象强行获取
3.请求和保持
线程1获得对象A并加锁A之后,还想要请求另外一个对象B(该对象被其他线程添加锁B),并且锁A并没有释放,即并没有因为想要请求锁B而释放锁A
4.循环等待
线程1在获取A的时候,同时请求B的时候等待线程2释放B,线程2在获取B的时候,同时请求A的时候等待线程1释放A(类似上面的吃饭问题)
五、如何破除死锁
在上面的死锁的四个必要条件中,三个都是锁的基本特性,只有一个条件是跟代码结构有关的— 循环等待,所以破除死锁的办法就是打破循环等待,可以给锁添加编号,指定一个固定的顺序(从大到小,或从小到大)来加锁,当任意线程增加锁时,都让线程遵守上面的顺序,就可以破除循环,比如上图中的哲学家就餐问题中给每根筷子编号,并且先拿小编号,再拿大编号,就不会产生死锁