目录
一. 死锁出现的场景
1. 一个线程, 一个锁对象
2. 两个线程, 两个锁对象
3. N个线程, M个锁对象
二. 造成死锁的必要条件
1. 锁是互斥的
2. 锁是不可被抢占的
3.请求和保持
4. 循环等待
三. 死锁的解决方案
1. 预防死锁
2. 死锁产生后的解决
一. 死锁出现的场景
1. 一个线程, 一个锁对象
当程序中只有一个线程和一个锁对象的时候, 如果这个线程针对这把锁连续加锁两次, 那么就会出现"死锁"的情况.
java"> void func() {
synchronized(this) { //第一层锁
synchronized(this) { //第二层锁
System.out.println("死锁");
}
}
}
如上述代码, 当某一个对象A调用这个方法的时候: 外层的synchronized直接拿到对象A, 顺利执行里面的语句, 执行到内层的synchronized时候, 此时对象A已经被占用, 那么此时内层的synchronized就会阻塞等待, 等待外层的synchronized释放对象A, 而外层的synchronized要释放对象A, 就必须把内层的synchronized执行完. 但是, 内层的synchronized执行完又要外层的synchronized把锁资源释放. 这样就形成了一个死循环, 当代码走到内层的synchronized的时候就会一直阻塞等待. 这就是"死锁"问题.
但是, 这样的逻辑放到java代码中并不会出现死锁问题, 这是因为java底层针对这种情况设计了"可重入锁", "可重入锁"针对两次加锁和多次加锁的情况做了特殊处理. 当一个线程第一次加锁的时候, 会加锁成功, 当后续再再有线程对相同的锁对象加锁时, 系统就会判断当前进行加锁的线程是否是当前锁持有的线程(前一次对这个锁对象加锁的线程), 如果是, 那么这个锁不会进行任何的阻塞操作, 而是"放行", 继续执行里面的代码. 示意图如下:
类似地, 如果嵌套多层: 每次要进行加锁的线程,要获取的锁对象都和前面一样的话, 那么真正加锁的只有最外层, 真正释放锁资源的也只有最外层. 示意图如下:
然而, 在C++ / Python中, 并没有像java这样的机制, 所以1遇到这样的情况就真的会出现"死锁问题". 程序会一直处于"阻塞等待"状态.
2. 两个线程, 两个锁对象
当有两个线程, 两把锁子时:
第一步: 线程1对锁对象A加锁, 线程2对锁对象B加锁.
第二步: 线程1在不释放A的情况下, 再对B加锁; 线程2在不释放B的情况下, 再对A加锁.
这样操作也会造成"死锁"问题.
具体演示代码如下:
java">public class Demo17 {
private static Object locker1 = new Object();
private static Object locker2 = new Object();
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
synchronized (locker1) {
System.out.println("t1 加锁 locker1 完成");
// 这里的 sleep 是为了确保, t1 和 t2 都先分别拿到 locker1 和 locker2 然后在分别拿对方的锁.
// 如果没有 sleep 执行顺序就不可控, 可能出现某个线程一口气拿到两把锁, 另一个线程还没执行呢, 无法构造出死锁.
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
synchronized (locker2) {
System.out.println("t1 加锁 locker2 完成");
}
}
});
Thread t2 = new Thread(() -> {
synchronized (locker1) {
System.out.println("t2 加锁 locker1 完成");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
synchronized (locker2) {
System.out.println("t2 加锁 locker2 完成");
}
}
});
t1.start();
t2.start();
}
}
t1线程拿到locker1后, 想拿locker2, 但是locker2正在被t2线程占有, 需要等待t2线程释放locker2. 而t2线程要能释放locker2, 就必须执行完语句, 必须拿到locker1. 而locker1要想释放t1线程就必须执行完语句, 就必须拿到locker2. 所以现在程序又陷入了一个"死循环"状态. 程序会一直处于"阻塞等待"状态, 形成"死锁".
通过运行结果, 我们看到, t1加锁locker1, t2加锁locker2之后, 程序就形成了"死锁", 进入了"阻塞等待"状态, 不会再往下执行了.
3. N个线程, M个锁对象
跟上面的第二种情况一样, 这种情景下每个线程需要拿到两个锁, 才能正常的执行完代码. 其他情况都会出现"死锁".
那么我们如何能保证每个线程都能顺利拿到两个锁对象呢? --> 这里就涉及到了操作系统中一个非常经典的问题: 哲学家就餐问题.
现在有五个哲学家围着一个圆桌, 圆桌中心有一份面, 没两个哲学家中间只有一根筷子. 那么我们如何保证每个哲学家都能顺利地吃到面呢?
我们可以约定, 让哲学家每次拿筷子的时候, 只能拿编号较小的筷子, 那么1, 2号哲学家都会尝试拿1号筷子, 3号哲学家尝试拿2号筷子, 4号哲学家尝试拿3号筷子, 5号哲学家尝试拿4号筷子. 那么这样一来, 5号筷子就空闲了. 所以5号哲学家就会同时拿起4号和5号筷子, 执行完他的任务, 然后放下4号和5号筷子. 此时4号哲学家就会拿起3号和4号筷子, 执行完任务之后放下筷子. 然后3号哲学家拿起2号和3号筷子 ...... 最终1号哲学家拿起1号和5号筷子执行完它的任务, 所有线程顺利执行完毕.
二. 造成死锁的必要条件
1. 锁是互斥的
锁的互斥性是锁的基本特性. 指一个锁资源同一时间只能被一个线程拿到. 一旦一个线程占用了这个锁资源, 其他线程必须等待, 直到这个锁资源资源被释放.
2. 锁是不可被抢占的
锁的不可抢占性也是锁的基本特性. 当某一线程占用了一个锁资源时, 另一线程不能强行抢占这个锁资源. 必须等待这个锁资源被释放.
3.请求和保持
线程t1, 在拿到锁A并且没有释放锁A的前提下, 去拿锁B, 就会产生死锁. (如果线程t1先释放了A, 再去拿B, 是没有任何问题的)
4. 循环等待
线程t1等待线程t2占有的资源, 线程t2等待线程t1占有的资源. 这样就产生了相互等待, 循环依赖的状况. 循环等待的状况一旦产生, 就会一直持续下去, 造成"死锁".
三. 死锁的解决方案
1. 预防死锁
(二)中的四个条件是形成死锁的必要条件, 缺一不可. 不满足其中一个条件就不能形成死锁. 我们也可以根据这个, 破坏四个条件中的任意一个, 就能避免死锁问题的发生.
例如: 我们可以将资源有序分配, 避免因锁资源竞争而产生的等待. --> 线程必须按照特定的某种顺序请求资源 (我们可以对所有资源进行编号, 令线程只能按照编号的顺序请求资源).
2. 死锁产生后的解决
导致死锁产生之后, 只能通过人工干预来解决. 比如重启服务, 或者kill掉产生死锁的线程.
好了, 本篇文章就介绍到这里啦, 大家如果有疑问欢迎评论, 如果喜欢小编的文章, 记得点赞收藏~~