文章目录
- 1.什么是死锁
- 2.三个典型情况
- 3.可重入与不可重入
- 4.死锁的四个必要条件
- 5.如何破除死锁
1.什么是死锁
比如张三谈了一个女朋友,张三就对这个女朋友加锁了。
此时李四也看上了这个女生,但是他只能等待张三分手(解锁)后,才能和这个女生谈恋爱。
李四为了等待这个女生,错过了好多喜欢他的人,这里就相当于线程无法执行的后续工作,
此时就相当于是死锁了。
一旦程序出现死锁,就会导致线程无法执行后序工作了,此时程序必然会有严重的 bug 。
发生死锁的概率又是随机的,因此死锁是非常隐蔽,不容易被发现的。
2.三个典型情况
情况1:
一个线程如果有一把锁,连续加锁两次。如果这个锁是不可重入锁,就会死锁。
java 中的 synchronized 和 ReentrantLock 都是可重入锁,
因此这一种情况演示不了。
情况2:
两个线程两把锁,t1 和 t2 各自先针对 锁1 和 锁2 加锁,之后再尝试获取对方的锁。
比如说,张三的车钥匙锁在屋里了,而屋子的钥匙锁在车了。
这个时候屋子和车都进不去了,就会产生问题。
下面来举例说明。
package thread;
public class ThreadDemo16 {
public static void main(String[] args) {
Object locker1 = new Object();
Object locker2 = new Object();
Thread t1 = new Thread(() -> {
synchronized (locker1) {
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (locker2) {
System.out.println("线程t1拿到两个锁");
}
}
});
Thread t2 = new Thread(() -> {
synchronized (locker2) {
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (locker1) {
System.out.println("线程t2拿到两个锁");
}
}
});
t1.start();
t2.start();
}
}
这里并没有输出结果,说明线程并没有拿到两把锁。
这个时候可以使用 jconsole 来查看当前的进程的情况。
按照这样的路径查找 jconsole ,然后双击。
看到这样的窗口,双击选中的进程。
红色框框的表示 获取锁获取不到的阻塞状态。
绿色框框的表示 发生错误的代码行数。
情况3: 多个线程,多把锁。(向较与情况2的一半情况)
例子:哲学家就餐问题
每个哲学家有两种状态:
- 思考人生(相当于线程的阻塞状态)
- 拿起筷子吃面条(想当于线程获取到锁然后执行一些操作)、
由于操作系统的随机调度,这五个哲学家,随时都可能想吃面条,也随时可能要思考人生。
如果想吃面条就需要拿起左手和右手的筷子。
假如同一时刻,所有的哲学家都拿起左手的筷子吃面条。
此时如果要成功吃到面条,就要等到右边的哲学家放下手中的筷子,自己才可以吃到面条。
此时就会死锁!!!
只有一只筷子没有办法吃面条,必须要等到右边的老铁放下筷子才可以吃。
如果右边一直不放,左边的老铁就一直吃不到。
3.可重入与不可重入
一个线程针对同一个对象,如果不会发生问题就叫可重入的,否则就叫不可重入的。
class Counter {
public int count = 0;
synchronized public void add() {
synchronized(this) {
count++;
}
}
}
锁对象是 this ,只要有线程调用 add 方法,进入 add 方法的时候,
在可以加锁成功的前提下,就会先加锁。紧接着又遇到了代码块,此时会再次尝试加锁。
站在 锁对象的视角(this),他认为自己已经被其他的线程给占用了,
那么这里的第二次加锁是不需要阻塞等待的。
如果允许上述操作,这个锁就是可重入的;不允许就是不可重入的。
如果是不可重入的,就会发生 死锁。
上面演示的就是不可重入的死锁。
下面演示的是可重入的思索。
java 为了避免不小心出现闭锁现象,就把 synchronized 给设置成可重入的了。
因此 java 中才会无法演示上面的情况1.
4.死锁的四个必要条件
1、互斥使用 — 线程1拿到了锁,线程1就需要等待着。
2、不可抢占 — 线程1拿到锁之后,如果线程1不释放锁,线程2就不能强行获取。
3、请求和等待 — 线程1获取到锁A之后,再去获取到锁B,
此时锁A还会继续被线程1获取。(不会因为获取锁B后就把锁A给释放了)
4、循环等待 — 线程1尝试获取到锁A和锁B,线程2尝试获取到锁B和锁A。
线程1在获取B的时候等待线程2释放B,同时线程2在获取A的时候等待线程1释放A。
5.如何破除死锁
打破循环等待这个必要条件。
解决办法:
给每个筷子编号,指定固定的顺序(从小到大)拿筷子。
上图是规定从小到大的拿。
到最后一个老铁拿的时候,会拿一号筷子。
但是这个一号筷子被其他的老铁拿了,此时这个老铁就发生阻塞等待了。
此时拿四号筷子的老铁会把五号筷子也拿了,之后开始吃面条。
这个老铁吃面条的时候,拿三号筷子的老铁就会看着他吃。
等待这个老铁吃完,放下两支筷子号筷子,三号筷子的老铁就可以拿起四号筷子来吃了。
按照这样的方式,所有的老铁都可以吃面条。
下面由代码来演示:
package thread;
public class ThreadDemo16 {
public static void main(String[] args) {
Object locker1 = new Object();
Object locker2 = new Object();
//给两个锁编号:1 、2,规定locker1是1号,locker2是2号,按照从小到大的顺序拿
Thread t1 = new Thread(() -> {
//先拿序号小的
synchronized (locker1) {
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
//后拿序号大的
synchronized (locker2) {
System.out.println("线程t1拿到两个锁");
}
}
});
Thread t2 = new Thread(() -> {
synchronized (locker1) {
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (locker2) {
System.out.println("线程t2拿到两个锁");
}
}
});
t1.start();
t2.start();
}
}
这种方法是解决死锁,最简单最可靠的方法。