长沙网站搭建seo,东莞发布解封通告,重庆app外包,郑州网站seo排名目录 死锁 1.构成死锁的场景 (1) 一个线程一把锁 问题描述 解决方案(可重入锁) (2) 两个线程两把锁 问题描述 (3)N个线程 M把锁 哲学家就餐问题 2.死锁的四个必要条件 3.如何解决死锁问题 (1)避免出现请求和保持 (2)打破多个线程的循环等待关系 死锁… 目录 死锁 1.构成死锁的场景 (1) 一个线程一把锁 问题描述 解决方案(可重入锁) (2) 两个线程两把锁 问题描述 (3)N个线程 M把锁 哲学家就餐问题 2.死锁的四个必要条件 3.如何解决死锁问题 (1)避免出现请求和保持 (2)打破多个线程的循环等待关系 死锁 1.构成死锁的场景 (1) 一个线程一把锁 问题描述 我们先来看下面的代码
看起来是两次对同一个引用的加锁是没有必要的但是在我们日常学习和工作中很容易就会写出上述的对一个锁对象进行两次或者多次加锁的操作此时会出现如下情况 如果第一次加锁要解锁就必须得先执行完中的代码块就必须进行第二次加锁但是第二次加锁时发现锁对象locker还未被解锁第二次加锁因此进入阻塞等待的状态所以第一次加锁的操作无法执行到解锁的位置上面的这种情况就被称为“死锁”dead lock;死锁是一个非常严重的 bug 一旦出现整个线程都会被卡住。 上面的代码虽然会造成死锁但是我们不太容易写出上面的代码但是一旦方法调用的层次比较深就容易出现对同一对象进行多次加锁的情况。 我们再来分析下面的代码 第一次进行加锁操作能够成功的(锁对象还没有被获取)第二次进行加锁此时意味着锁对象是已经被占用的状态第二次加锁就会触发阻塞等待。 解决方案(可重入锁) 为了解决上述代码出现的死锁问题 Java 的 synchronized 就引入了可重入的概念 当 t线程 对 locker对象 加锁成功之后后续 t 再次针对 locker 进行加锁不会触发阻塞而是直接往下走因为当前 locker 就是被 t 持有~~ 但是如果是其他线程尝试加锁就会正常进入阻塞等待的状态 如果发现是同一个锁持有者的线程则跳过加锁环节如果是不同的锁持有者才会进入阻塞等待。 我们运行刚刚所写的代码发现程序是可以正常执行的 理论上程序会被上死锁但是当我们正在运行程序时会发现程序依旧可以正常执行输出结果也正确这样的原因是因为 synchronized 的可重入性解决了当前情况一个线程针对同一个锁对象进行多次加锁造成的死锁的问题哪怕我们再锁三四层synchronized 的可重入性都会解决该问题。 可重入锁只能针对 一个线程多次对锁对象进行加锁 的情况如果是其他情况造成的死锁则无法通过可重入锁解决。 面试官的问题: 如何自己实现一个可重入锁? 在锁内部记录当前是哪个线程持有的锁后续每次加锁都进行判定通过计数器记录当前加锁的次数从而确定何时真正进行解锁. (2) 两个线程两把锁 问题描述 现在有 t1t2 两个线程以及 locker1locker2 两把锁t1 获取 locker1t2 获取 locker2 后t1t2再分别尝试获取 locker2locker1两个线程互不相让因此进入阻塞等待最终造成死锁的情况家钥匙放车里车钥匙放家里 因为上述这种情况构成的死锁问题的原因不但因为锁互斥与不可抢占的性质也因为两个线程在加锁的过程中造成了请求保持和循环等待
而造成死锁的原因是因为两个线程对两个锁对象的加锁是嵌套的写法。 我们来看下面这段代码
package Thread;public class Demo22 {public static void main(String[] args) throws InterruptedException {Object locker1 new Object();Object locker2 new Object();Thread t1 new Thread(() - {System.out.println(t1 获取到 locker1);synchronized (locker1){try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}synchronized (locker2){System.out.println(t1 获取到 locker2);}}});Thread t2 new Thread(() -{synchronized (locker2){System.out.println(t2 获取到 locker2);try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}synchronized (locker1){System.out.println(t2 获取到 locker1);}}});t1.start();t2.start();t1.join();t2.join();}
}代码逻辑 t1 线程 和 t2 线程 在分别获取到 locker1locker2 之后打印日志打印日志后两个线程分别休眠 1s 休眠的目的是为了防止系统随机调度线程抢占式执行使得其中一个线程一口气获取到 locker1locker2两把锁在休眠结束后两个线程分别尝试获取对方已经获取过且没有释放的锁。 我们运行程序并提供 jconsole 查看 t1t2 的状态 造成死锁之后t1t2 都进入阻塞等待的状态从执行结果的打印日志来看整个进程被死锁卡在两个线程各自加第二把锁的时候jconsolo 的堆栈跟踪也一目了然地表明情况。 (3)N个线程 M把锁 哲学家就餐问题 大部分情况下上述模型可以很好的运作但是在一些极端的情况下会造成死锁上面的五个线程都在获取一把锁后尝试对另一把已经被别的线程获取过的锁进行加锁因此所有线程都陷入了阻塞等待的状态并且五个线程五把锁之间的等待过程构成了循环。
这也使得线程与线程之间出现了请求保持的情况
多个线程多把锁出现死锁的情况如上面吃面造成死锁的这种情况是比较典型极端的当然还有更多种出现死锁的情况
我们先要处理典型的情况如刚刚吃面的问题虽然这种情况可能性很小但是也不能忽略这种情况。 2.死锁的四个必要条件 对于我们在上面描述的构成死锁的三个场景中只要涉及 N 个线程M把锁N1 M1并且产生了死锁原因都是满足了上述的四个必要条件。 3.如何解决死锁问题 刚刚构成死锁四个必要条件锁的互斥与不可抢占是因为锁的基本特性要通过解决锁的互斥和不可抢占来解决死锁问题的做法非常难要想打破死锁避免死锁我们应该从请求与保持或者循环等待这两个构成死锁的原因来寻找突破点。只要能够解决请求与保持或者循环等待两个原因中的任意一个就能够打破死锁~ (1)避免出现请求和保持 我们再来分析一下产生死锁问题的原因是因为请求保持的代码 我们可以发现 上述代码中的两个线程无论是 t1 还是 t2在进行加锁的代码块中加锁的方式都是嵌套加锁这就使得两个线程无法获取对方的锁又无法解锁从而双双进入阻塞等待
换句话说这种构成死锁情况的原因就叫做请求保持 解决方法 对于上图的代码我们要对其进行修改
对于t1线程把synchronized(locker2) 从 synchronized(locker1)的大括号代码块中取出 synchronized(locker2) 和 synchronized(locker1) 在 t1 线程中从嵌套关系变成并列关系对于 t2 线程也作出同样的修改使得加锁方式从嵌套加锁修改为并列加锁 执行结果 所以要想解决请求保持就不要写出嵌套加锁的代码但是在日常开发中确实会出现代码逻辑必须要通过嵌套加锁来完成一些操作所以嵌套加锁很难避免
因此我们更通用的打破死锁的做法就是打破多个线程之间的循环等待关系。 (2)打破多个线程的循环等待关系 我们把刚刚的并列加锁代码还原成嵌套加锁 只要涉及 N 个线程M把锁N1 M1都可以用“哲学家就餐”模型来进行描述 只要我们对线程的加锁的顺序做出约定所有的线程都按照一定顺序进行加锁就可以破除循环等待条件进而打破死锁 (1) (2) (3) (4) (5) (6) (7) 此时看着桌子上所剩不多的 CPU 资源t1瞬间黑化成邪恶栀子花 通过上述“哲学家就餐”模型我们能直观的发现只要规定好加锁顺序就可以打破多个线程循环等待的关系进而解决死锁问题。 我们回归代码来感受一下约定加锁顺序规定每个线程先获取编号小的锁再去获取编号大的锁)后带来的效果
因为五个线程五把锁的情况并不容易产生死锁所以我们就用场景二来演示 对于上述代码我们约定每个线程都先获取编号小的锁对象t1 先获取 lcoker1再获取 locker2满足约定的规则t2 先获取 locker2再获取 locker1不满足约定的规则所以需要对 t2 进行修改。 执行结果
约定加锁顺序后通过修改后代码的执行结果我们可以看到 死锁的问题就被完美的解决了~