• Java进阶篇--死锁


    目录

    导致死锁的原因

    避免死锁的方法

    代码示例


    死锁是指两个或多个线程在互相等待对方释放资源的情况下,无法继续执行的状态。当发生死锁时,线程将永远阻塞,程序也无法正常完成。

    导致死锁的原因

    死锁是多线程编程中的一种常见问题,它发生在两个或多个线程彼此持有对方所需资源的情况下,导致它们都无法继续执行。这些线程被称为相互等待对方的资源,从而形成了死锁状态。

    导致死锁的原因通常可以归结为以下四个必要条件的同时满足:

    1. 互斥条件(Mutual Exclusion):一个资源每次只能被一个线程占用。当一个线程获得了资源的独占权后,其他线程就无法再访问该资源,直到该线程释放资源。
    2. 请求与保持条件(Hold and Wait):一个线程在持有了至少一个资源的同时又请求其他资源,而这些资源被其他线程所占用。当这种情况发生时,如果不能立即获得所需的资源,就会阻塞并等待其他线程释放资源。
    3. 不可剥夺条件(No Preemption):资源只能在线程自愿释放时才能被其他线程占用,其他线程无法强行将其剥夺。即线程需要使用完所有获取到的资源后才会主动释放资源,不会被其他线程打断。
    4. 循环等待条件(Circular Wait):存在多个线程之间形成一个环形链,每个线程都在等待下一个线程所持有的资源。例如,线程 A 等待线程 B 持有的资源,线程 B 等待线程 C 持有的资源,以此类推,直到线程 N 等待线程 A 持有的资源。

    只有当以上四个条件同时满足时,死锁才会发生。解决死锁问题的方法通常包括破坏其中一个或多个必要条件。

    避免死锁的方法

    避免死锁是多线程编程中重要的问题之一,下面介绍几种常见的避免死锁的方法:

    1. 加锁顺序:确保所有线程以相同的顺序获得锁。通过强制要求线程按照特定的顺序获取资源锁,可以避免循环等待条件的发生。
    2. 加锁时限:对获取不到所需资源的线程设置一个超时时间,在超过一定时间后如果仍未获取到资源,则释放已经占用的资源,避免持有并等待条件的发生。这种方法需要谨慎使用,需要根据具体场景来确定超时时间的合理性,并避免引入新的问题。
    3. 死锁检测:实时监控程序运行状态,检测是否存在死锁。一旦检测到死锁,系统可以采取一些恢复措施,例如强制释放某些资源或者重启线程等。
    4. 资源分配策略:通过合理的资源分配策略来预防死锁。例如,银行家算法(Banker's Algorithm)用于在分布式系统中避免死锁,通过动态分配资源并根据系统需要进行资源回收。
    5. 避免持有并等待条件:线程在请求新的资源之前释放已占有的资源。这可以通过设计合适的资源分配算法来实现,例如按照资源请求层级来分配资源,或者采用资源预先分配的方式。
    6. 使用互斥量和条件变量:互斥量和条件变量是常用的同步机制,在使用时需要合理地获取和释放锁,以遵循加锁顺序,避免死锁的发生。

    需要注意的是,以上方法并非适用于所有情况,具体的应用需要根据具体的需求和场景来选择合适的策略。在设计多线程程序时,合理的资源管理和同步机制是避免死锁问题的关键。同时,定期进行代码审查、测试和性能优化也是保证程序健壮性和可靠性的重要手段。

    代码示例

    以下是同时包含死锁和避免死锁的代码示例,

    1. public class main {
    2. private static final Object resource1 = new Object();
    3. private static final Object resource2 = new Object();
    4. public static void main(String[] args) {
    5. Thread thread1 = new Thread(() -> {
    6. synchronized (resource1) { // 获取资源1的锁
    7. System.out.println("线程 1:持有资源 1");
    8. try {
    9. Thread.sleep(1000);
    10. } catch (InterruptedException e) {
    11. e.printStackTrace();
    12. }
    13. System.out.println("线程 1:等待资源 2");
    14. synchronized (resource2) { // 尝试获取资源2的锁
    15. System.out.println("线程 1:持有资源 1 和资源 2");
    16. }
    17. }
    18. });
    19. Thread thread2 = new Thread(() -> {
    20. synchronized (resource1) { // 尝试获取资源1的锁
    21. System.out.println("线程 2:持有资源 1");
    22. try {
    23. Thread.sleep(1000);
    24. } catch (InterruptedException e) {
    25. e.printStackTrace();
    26. }
    27. System.out.println("线程 2:等待资源 2");
    28. synchronized (resource2) { // 获取资源2的锁
    29. System.out.println("线程 2:持有资源 1 和资源 2");
    30. }
    31. }
    32. });
    33. thread1.start();
    34. thread2.start();
    35. // 等待线程执行完成
    36. try {
    37. thread1.join();
    38. thread2.join();
    39. } catch (InterruptedException e) {
    40. e.printStackTrace();
    41. }
    42. // 避免死锁的示例
    43. Object resourceA = new Object();
    44. Object resourceB = new Object();
    45. Thread thread3 = new Thread(() -> {
    46. synchronized (resourceA) { // 获取资源A的锁
    47. System.out.println("线程 3:持有资源 A");
    48. try {
    49. Thread.sleep(1000);
    50. } catch (InterruptedException e) {
    51. e.printStackTrace();
    52. }
    53. System.out.println("线程 3:等待资源 B");
    54. synchronized (resourceB) { // 尝试获取资源B的锁
    55. System.out.println("线程 3:持有资源 A 和资源 B");
    56. }
    57. }
    58. });
    59. Thread thread4 = new Thread(() -> {
    60. synchronized (resourceA) { // 获取资源A的锁
    61. System.out.println("线程 4:持有资源 A");
    62. try {
    63. Thread.sleep(1000);
    64. } catch (InterruptedException e) {
    65. e.printStackTrace();
    66. }
    67. System.out.println("线程 4:等待资源 B");
    68. synchronized (resourceB) { // 尝试获取资源B的锁
    69. System.out.println("线程 4:持有资源 A 和资源 B");
    70. }
    71. }
    72. });
    73. thread3.start();
    74. thread4.start();
    75. }
    76. }

    在这个例子中,一开始的两个线程 thread1 和 thread2 尝试获取 resource1 和 resource2 的锁以形成死锁。为了避免死锁,我们在这两个线程中修改了获取资源的顺序。其中一个线程先获取 resource1,另一个线程再获取 resource1,这样能够保证资源的获取顺序是一致的,避免了循环等待。

    在后面的代码示例中,我们通过尝试获取不同的资源来模拟死锁情况。为了避免死锁,我们采取了避免持有并请求条件的策略,即一个线程只能在释放所有资源之后再请求新的资源。这样能够确保资源的占用和释放是一致的,不会出现死锁的情况。

    注意,在上面的示例中,为了能够观察到死锁和避免死锁的效果,我们需要等待线程执行完成。因此,我们在两个线程启动之后使用 join() 方法来等待它们执行完成。

  • 相关阅读:
    Manifest merger failed
    arcgis一个面中剔除多个点仅保留一个点的方法
    Zookeeper - 节点
    远程连接服务器的方法(VScode、Xshell、Ubuntu)
    99%的人都把三层架构和SpringMVC的关系搞错了
    Linux hook 技术一个简单demo分析
    YOLO V5源码详解
    Lumiprobe细胞生物学丨DiA,亲脂性示踪剂说明书
    软件测试常用的功能测试方法
    新火种AI|苹果要将苹果智能做成AI时代的APP Store?
  • 原文地址:https://blog.csdn.net/m0_74293254/article/details/133864006