• 一个合格的后端开发总要经历一次死锁吧


            我相信大家在开发中应该都或多或少的使用过线程吧 , 那么如果在使用线程中再有因为并发情况需要加锁 , 那就避不开一个问题 , 就是死锁. , 没遇到过也无妨, 接下来我会用一个小demo来模拟死锁 , 并提供解决办法 ,

    一, 什么是死锁

            死锁是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程。例如,如果进程A锁住了记录1并等待记录2,而进程B锁住了记录2并等待记录1,这样两个进程就发生了死锁现象。

     二, 代码重现

            小黄和小卢同时去吃一份汉堡可乐, 然后小黄和小卢分别同时拿到了汉堡和可乐, 然后小黄又去拿可乐, 而这时候可乐再小卢手里 , 小卢也同时去哪汉堡 , 相同的汉堡在小黄手里 , 然后他们彼此还都不把手里现有的放回去 , 死锁就出现了

    1. /**
    2. * @author huang
    3. */
    4. public class DeadLock{
    5. /**
    6. * 汉堡
    7. */
    8. public static Object hamburger = new Object();
    9. /**
    10. * 可乐
    11. */
    12. public static Object coke = new Object();
    13. public static void main(String[] args) {
    14. new Huang().start();
    15. new Lu().start();
    16. }
    17. private static class Huang extends Thread {
    18. @Override
    19. public void run() {
    20. synchronized (hamburger) {
    21. System.out.println("小黄吃到了汉堡");
    22. try {
    23. Thread.sleep(1000);
    24. } catch (InterruptedException e) {
    25. System.out.println("小黄被中断了!");
    26. }
    27. System.out.println("小黄正在等可乐");
    28. synchronized (coke) {
    29. System.out.println("小黄吃到了可乐");
    30. try {
    31. Thread.sleep(1000);
    32. } catch (InterruptedException e) {
    33. System.out.println("小黄被中断了");
    34. }
    35. }
    36. System.out.println("小黄放回去了可乐");
    37. }
    38. System.out.println("小黄放回去了汉堡");
    39. }
    40. }
    41. private static class Lu extends Thread {
    42. @Override
    43. public void run() {
    44. synchronized (coke) {
    45. System.out.println("小卢吃到了可乐");
    46. try {
    47. Thread.sleep(1000);
    48. } catch (InterruptedException e) {
    49. System.out.println("小卢被中断了!");
    50. }
    51. System.out.println("小卢正在等汉堡");
    52. synchronized (hamburger) {
    53. System.out.println("小卢吃到了汉堡");
    54. try {
    55. Thread.sleep(1000);
    56. } catch (InterruptedException e) {
    57. System.out.println("小卢被中断了!");
    58. }
    59. }
    60. System.out.println("小卢放回去了汉堡");
    61. }
    62. System.out.println("小卢放回去了可乐");
    63. }
    64. }
    65. }

    此时控制台情况如下 , 双方都在等资源, 然后双方已拿到的又都不松手 , 形成了互相等待的局面, 而main函数也因这两个线程的阻塞 无法正常退出.

     那么这个时候问题来了 , 如果说这是线上环境呢 ? 你应该怎么去确认是否存在线程死锁呢 ? 

    三, 问题排查

            这个时候就考验java基础扎实不扎实了 

    3.1 死锁产生原因

            虽然进程在运行过程中,可能发生死锁,但死锁的发生也必须具备一定的条件,死锁的发生必须具备以下四个必要条件

    1)互斥条件:指进程对所分配到的资源进行排它性使用,即在一段时间内某资源只由一个进程占用。如果此时还有其它进程请求资源,则请求者只能等待,直至占有资源的进程用毕释放。(小黄小卢都有再用汉堡和可乐两把锁)

    2)请求和保持条件:指进程已经保持至少一个资源,但又提出了新的资源请求,而该资源已被其它进程占有,此时请求进程阻塞,但又对自己已获得的其它资源保持不放。(小黄和小卢都是先拿到其中一个然后在拿第二个)

    3)不剥夺条件:指进程已获得的资源,在未使用完之前,不能被剥夺,只能在使用完时由自己释放。(小黄和小卢都是只有当拿到第二件且执行结束自己的逻辑才会释放前锁)

    4)环路等待条件:指在发生死锁时,必然存在一个进程——资源的环形链,即进程集合{P0,P1,P2,···,Pn}中的P0正在等待一个P1占用的资源;P1正在等待P2占用的资源,……,Pn正在等待已被P0占用的资源。(顺序不一致, 导致形成闭环)

    以下是图解死锁 , 可以更直观的去理解

     

     3.2 jstack

            俗话说得好 , 工欲善其事必先利其器 , 而这时候我们的器就是 Jstack

            jstack是JVM自带的Java堆栈跟踪工具,它用于打印出给定的java进程ID、core file、远程调试服务的Java堆栈信息.

    • jstack命令用于生成虚拟机当前时刻的线程快照。
    • 线程快照是当前虚拟机内每一条线程正在执行的方法堆栈的集合,生成线程快照的主要目的是定位线程出现长时间停顿的原因, 如线程间死锁、死循环、请求外部资源导致的长时间等待等问题。
    • 线程出现停顿的时候通过jstack来查看各个线程的调用堆栈,就可以知道没有响应的线程到底在后台做什么事情,或者等待什么资源。
    • 如果java程序崩溃生成core文件,jstack工具可以用来获得core文件的java stack和native stack的信息,从而可以轻松地知道java程序是如何崩溃和在程序何处发生问题。
    • 另外,jstack工具还可以附属到正在运行的java程序中,看到当时运行的java程序的java stack和native stack的信息, 如果现在运行的java程序呈现hung的状态,jstack是非常有用的。

    最常用的是

    jstack [option] <pid>  // 打印某个进程的堆栈信息

     这时候咱们就可以线使用 JPS 命令列出来当前正在运行的Java进程 ,  

    找到我们的main函数所在的类 , pid 是34448 , 然后使用 Jstack 34448 查看当前运行情况

     

     found one java-level deadlock , 发现一个死锁  , 并且列出了死锁的具体信息 , 

    其中 这部分信息是说明当前死锁的两个线程 分别在等待的锁

    Thread-0  在等待  (object 0x0000000622ea3270, a java.lang.Object) 这个锁, 而这把锁正在被Thread-1 持有, 另一个线程反之

    1. =============================
    2. "Thread-0":
    3. waiting to lock monitor 0x000001cb73be1300 (object 0x0000000622ea3270, a java.lang.Object),
    4. which is held by "Thread-1"
    5. "Thread-1":
    6. waiting to lock monitor 0x000001cb73be1100 (object 0x0000000622ea3260, a java.lang.Object),
    7. which is held by "Thread-0"

     然后下面列出了具体发生的位置 , 和等待锁 , 当前持有锁的信息 

    1. "Thread-0":
    2. at com.ko.assy.db.ms.controller.demo.DeadLock$Huang.run(DeadLock.java:36)
    3. - waiting to lock <0x0000000622ea3270> (a java.lang.Object)
    4. - locked <0x0000000622ea3260> (a java.lang.Object)
    5. "Thread-1":
    6. at com.ko.assy.db.ms.controller.demo.DeadLock$Lu.run(DeadLock.java:61)
    7. - waiting to lock <0x0000000622ea3260> (a java.lang.Object)
    8. - locked <0x0000000622ea3270> (a java.lang.Object)

    四, 死锁问题修复

            理解了死锁的原因,尤其是产生死锁的四个必要条件,就可以最大可能地避免、预防和解除死锁。只要打破四个必要条件之一就能有效预防死锁的发生:打破互斥条件:改造独占性资源为虚拟资源,大部分资源已无法改造。打破不可抢占条件:当一进程占有一独占性资源后又申请一独占性资源而无法满足,则退出原占有的资源。打破占有且申请条件:采用资源预先分配策略,即进程运行前申请全部资源,满足则运行,不然就等待,这样就不会占有且申请。打破循环等待条件:实现资源有序分配策略,对所有设备实现分类编号,所有进程只能采用按序号递增的形式申请资源

    4.1有序资源分配 

            这种算法资源按某种规则系统中的所有资源统一编号(例如打印机为1、磁带为2、磁盘为3、等等),申请时必须以上升的次序。系统要求申请进程:

    1、对它所必须使用的而且属于同一类的所有资源,必须一次申请完;

    2、在申请不同类资源时,必须按各类设备的编号依次申请。例如:进程PA,使用资源的顺序是R1,R2; 进程PB,使用资源的顺序是R2,R1;若采用动态分配有可能形成环路条件,造成死锁。

    采用有序资源分配法:R1的编号为1,R2的编号为2;

    PA:申请次序应是:R1,R2

    PB:申请次序应是:R1,R2

    这样就破坏了环路条件,避免了死锁的发生

    4.2 破坏循环等待条件

            若是每个线程都依赖上一线程所持有的资源,那么整个线程链就会像闭环的贪吃蛇一样,导致资源无法被释放。因此就需要某一个线程释放资源,从而打破循环。

            所以,我们平时的代码要如何设计才能尽量避免死锁的发生呢?

    尽量将程序设置为可中断的

            将程序设置为可中断的,这样在死锁环境下如果某个线程收到中断请求之后就可以主动地释放掉手中的资源。

            Java多线程中有一个重要的方法interrupt(),这个方法可以请求调用此方法的线程触发中断机制,该线程可以自身决定是否释放资源。若是已经发生了死锁,只要它放弃资源便可打破。

    4.3 为锁添加时限

    除此之外还可以为尝试获取锁的线程添加一个超时等待时间。若线程在规定时间内获取不到锁则放弃,这样就可以避免线程无脑请求,同时也会释放该线程已有的资源,让其它线程有机会获取到锁,可以开放化一个相对封闭的资源环境。

    五. 修复后运行

    采用有序资源分配解决问题

    1. package com.ko.assy.db.ms.controller.demo;
    2. /**
    3. * @author huang
    4. */
    5. public class UnDeadLock {
    6. /**
    7. * 汉堡
    8. */
    9. public static Object hamburger = new Object();
    10. /**
    11. * 可乐
    12. */
    13. public static Object coke = new Object();
    14. public static void main(String[] args) {
    15. new Huang().start();
    16. new Lu().start();
    17. }
    18. private static class Huang extends Thread {
    19. @Override
    20. public void run() {
    21. synchronized (hamburger) {
    22. System.out.println("小黄吃到了汉堡");
    23. try {
    24. Thread.sleep(1000);
    25. } catch (InterruptedException e) {
    26. System.out.println("小黄被中断了!");
    27. }
    28. System.out.println("小黄正在等可乐");
    29. synchronized (coke) {
    30. System.out.println("小黄吃到了可乐");
    31. try {
    32. Thread.sleep(1000);
    33. } catch (InterruptedException e) {
    34. System.out.println("小黄被中断了");
    35. }
    36. }
    37. System.out.println("小黄放回去了可乐");
    38. }
    39. System.out.println("小黄放回去了汉堡");
    40. }
    41. }
    42. private static class Lu extends Thread {
    43. @Override
    44. public void run() {
    45. synchronized (hamburger) {
    46. System.out.println("小卢吃到了汉堡");
    47. try {
    48. Thread.sleep(1000);
    49. } catch (InterruptedException e) {
    50. System.out.println("小卢被中断了!");
    51. }
    52. System.out.println("小卢正在等可乐");
    53. synchronized (coke) {
    54. System.out.println("小卢吃到了可乐");
    55. try {
    56. Thread.sleep(1000);
    57. } catch (InterruptedException e) {
    58. System.out.println("小卢被中断了!");
    59. }
    60. }
    61. System.out.println("小卢放回去了可乐");
    62. }
    63. System.out.println("小卢放回去了汉堡");
    64. }
    65. }
    66. }

    运行结果如下 

     

  • 相关阅读:
    光学知识整理-偏振光
    面向对象三大特征之一:继承
    工具及方法 - 查电子器件和查说明书
    【图书管理系统】附源码+教程
    CITE2022丨中科创达发布一站式交钥匙边缘计算解决方案 解锁边缘应用落地新方式
    Java程序设计——注解(Java高级应用)
    01. 汇编LED驱动实验
    网络安全态势感知运营中心建设解决方案
    【微信小程序入门到精通】— 微信小程序开发工具的安装
    期货开户趋势的本质是惯性
  • 原文地址:https://blog.csdn.net/qq_42543063/article/details/127851501