【问题标题】:Tips to prevent deadlocks in javajava中防止死锁的技巧
【发布时间】:2013-05-22 18:26:27
【问题描述】:

我正在研究java线程和死锁,我了解死锁的例子,但我想知道是否有一般规则可以防止它。

我的问题是是否有规则或提示可以应用于java中的源代码以防止死锁?如果是,您能解释一下如何实现它吗?

【问题讨论】:

    标签: java multithreading deadlock


    【解决方案1】:

    一些小窍门

    • 不要使用多线程(例如 Swing 所做的那样,强制所有事情都在 EDT 中完成)
    • 不要同时持有多个锁。如果这样做,请始终以相同的顺序获取锁
    • 持有锁时不要执行外部代码
    • 使用可中断锁

    【讨论】:

    • 点 (1):好的,像 EDT。 (2):如何按相同顺序获取线程? (3):好的,这点我明白了 4:什么是可中断锁?谢谢
    • 你能解释一下你所说的"foreign"代码是什么意思吗?进行网络调用的代码?调用那个块?调用不是你写的其他类?调用其他国家/地区编写的代码:) ...
    • 调用不是由您编写的类(如侦听器、观察者、策略,由 API 的调用者提供)。
    • 我们可能在持有锁的同时不执行外来代码的原因是,如果外来代码调用您的代码 A 的某些部分,那么该部分 A 可能会再次尝试请求您的锁,然后你的线程就会陷入死锁。这可能就是所谓的“自锁”。
    【解决方案2】:

    封装、封装、封装! 使用锁可能会犯的最危险的错误就是将您的锁公开(公开)。不知道如果你这样做会发生什么,因为任何人都可以在对象不知道的情况下获得锁(这也是你不应该锁定this 的原因)。如果您将锁保密,那么您将拥有完全的控制权,这使得它更易于管理。

    【讨论】:

      【解决方案3】:
      1. 使用无锁数据结构避免锁(例如,使用 ConcurrentLinkedQueue 而不是同步的 ArrayList
      2. 始终以相同的顺序获取锁,例如为每个锁分配一个唯一的数值,在获取数值较高的锁之前先获取数值较小的锁
      3. 在超时后释放您的锁(从技术上讲,这并不能防止死锁,只是有助于在死锁发生后解决)

      【讨论】:

      • 无锁数据结构不会导致死锁。 ConcurrentLinkedQueue 是无锁数据结构的一个示例——您可以从多个线程中的offerpoll 队列,而无需同步访问它
      【解决方案4】:

      阅读并理解Java:并发与实践。这不是关于避免死锁的“技巧”。我永远不会聘请知道一些避免死锁的技巧并且经常避免死锁的开发人员。这是关于理解并发性。幸运的是,有一本关于该主题的全面的中级书籍,所以去阅读吧。

      【讨论】:

        【解决方案5】:
        1. 不要使用锁。
        2. 如果必须,请将锁保留在本地。全局锁可能非常棘手。
        3. 握住锁时尽量少做。
        4. 使用stripes 仅锁定数据段
        5. 首选不可变类型。很多时候,这意味着复制数据而不是共享数据。
        6. 请改用比较和设置 (CAS) 机制,例如参见 AtomicReference

        【讨论】:

          【解决方案6】:

          在防止死锁方面几乎只有一条大规则:

          如果您需要在代码中拥有多个锁,请确保每个人始终以相同的顺序获取它们。

          不过,让您的代码免于锁定应该始终是您的目标。您可以尝试通过使用不可变或线程本地对象和无锁数据结构来摆脱它们。

          【讨论】:

          • “同顺序”是什么意思?你有一个例子吗?谢谢
          • @iberck 看这个典型的例子:stackoverflow.com/a/1385876/829571(它是伪代码,但你应该明白)。
          • 当然,但“如果你需要”部分是 75% 的工作发生的地方。您应该精通无锁模式。
          【解决方案7】:

          给定一个设计选择,在队列推送/弹出中只有锁的情况下使用消息传递。这并不总是可能的,但如果是这样,您将很少遇到死锁。你仍然可以得到它们,但你必须非常努力:)

          【讨论】:

          • 附议。我可以推荐 JCSP,它很好地形式化了它。请参阅 Wikipedia 上的页面,en.wikipedia.org/wiki/JCSP。 CSP 意味着通常您不必担心死锁、活锁等问题。正如 Wiki 文章所指出的,不使用 CSP 不可避免地会给您留下一个仅通过测试无法证明其正确性的程序。但是,对于 CSP,有一些简单的指导方针可以遵循,而且您不可避免地会没事的。如果你有勇气,你可以做 CSP 数学并证明你的程序是正确的,无需测试。见en.wikipedia.org/wiki/Communicating_sequential_processes
          • 哦,JCSP 的一个有用的副作用是您从一开始就将可伸缩性内置到您的程序中。线程可以成为单独计算机上的单独进程,但程序的整体架构保持不变。因此,从单台机器扩展到整个数据中心的价值变得可行,无需重新设计。
          • @bazza - 是的,几十年来我一直在使用消息传递设计,在 P-C 队列上通信对象,使用 Java、Delphi 和 C++。在两次我设法陷入僵局的情况下,一次只是我的愚蠢行为,另一次是我的糟糕设计导致 GUI 输入队列超载。
          【解决方案8】:

          布尔标志示例

          我喜欢这个例子。它启动两个共享布尔标志的线程:

          public class UntilYouUpdateIt 
          {
              public static boolean flag = true;
          
              public static void main(String[] args) throws InterruptedException 
              {
                  Thread t1 = new Thread(()->
                  {
                      while(flag){}
                      System.out.println("end");
                  });
                  t1.start();
          
                  Thread.sleep(100);
          
                  Thread t2 = new Thread(()->
                  {
                     flag = false;
                     System.out.println("changed");
                  });
                  t2.start();
                }
          }
          

          第一个线程将循环直到flag 为假,这发生在第二个线程的第一行。该程序永远不会完成,它的输出将是:

          changed
          

          第二个线程死亡,同时第一个线程将永远循环

          为什么会这样? Compiler opmitizations。 Thread1 将永远不会再次检查标志的值,如:

          • 循环内的操作很便宜(无事可做)
          • 编译器知道没有其他实体可以修改标志值(因为第一个线程没有,第二个已经死了)。所以它假定 flag 永远为真

          换句话说,Thread1 将始终从 cache 中读取 flag 值,该值设置为 true


          解决/测试此问题的两种方法:

              Thread t1 = new Thread(()->
              {
                  while(flag)
                  {
                     System.out.print("I'm loopinnnng");
                  }
                  System.out.println("end");
              });
          

          如果包含一些“繁重”操作(int i=1 或类似的操作都不起作用),例如 System 调用,优化器会更加小心,检查flag boolean 以了解他是否没有浪费资源。输出将是:

          I'm loopinnnng
          I'm loopinnnng
          I'm loopinnnng
          I'm loopinnnng
          I'm loopinnnng
          (....)
          changed
          end
          

          I'm loopinnnng
          I'm loopinnnng
          I'm loopinnnng
          I'm loopinnnng
          I'm loopinnnng
          (....)
          end
          changed
          

          取决于最后分配给哪个线程的 cpu 时间。

          在使用布尔变量时,避免此类死锁的正确解决方案应包括 volatile 关键字。

          volatile 告诉编译器:当涉及到这个变量时不要尝试优化。

          所以,同样的代码只添加了那个关键字:

          public class UntilYouUpdateIt 
          {
              public static volatile boolean flag = true;
          
              public static void main(String[] args) throws InterruptedException 
              {
                  Thread t1 = new Thread(()->
                  {
                      while(flag){}
                      System.out.println("end");
                  });
                  t1.start();
          
                  Thread.sleep(100);
          
                  Thread t2 = new Thread(()->
                  {
                     flag = false;
                     System.out.println("changed");
                  });
                  t2.start();
                }
          }
          

          将输出:

          changed
          end
          

          end
          changed
          

          结果是两个线程都正确完成,避免了任何死锁。


          无序锁示例

          这是一个基本的:

          public void  methodA() 
          {
            //...
          
            synchronized(lockA)
            {
               //...
           
               synchronized(lockB)
               {
                //...
               }
             }
          } 
          
          
          public void methodB() 
          {
            //...
          
            synchronized(lockB)
            {
               //...
            
              synchronized(lockA)
               {
                //...
               }
             }
          }
          

          如果被许多线程调用,这种方法可能会造成很大的死锁。这是因为对象以不同的顺序锁定。这是死锁最常见的原因之一,因此如果要避免死锁,请确保按顺序获取锁。

          【讨论】:

          • 这个问题的最好例子是两个银行账户试图同时互相转账。如果锁是静态的,那么很容易看出它们何时发生故障;如果它们是动态的,并且数据恰好可以互换,那么就没有那么多了。
          • 我很想听听反对者的一些解释。先生,在这个简单的例子中我到底在哪里不正确? :)
          【解决方案9】:

          Java 中的死锁是一种编程情况,其中两个或多个线程被永久阻塞。至少有两个线程和两个或更多资源时会出现 Java 死锁情况。

          如何检测 Java 中的死锁

          要检测Java中的死锁,我们需要查看应用程序的java线程转储,我们可以使用VisualVM分析器或使用生成线程转储jstack 实用程序。

          为了分析死锁,我们需要查看状态为BLOCKED的线程,然后是等待锁定的资源。每个资源都有一个唯一的 ID,使用它我们可以找到哪个线程已经持有对象的锁。

          如何避免java中的死锁

          这些是我们可以避免大多数死锁情况的一些准则。

          • 通过打破循环等待条件避免死锁:为了做到这一点,您可以在代码中进行安排,对获取和释放锁进行排序。如果锁将以一致的顺序获取并以相反的顺序释放,则不会出现一个线程持有由另一个线程获取的锁的情况,反之亦然。
          • 避免嵌套锁:这是最常见的死锁原因,如果您已经持有另一个资源,请避免锁定另一个资源。如果您只使用一个对象锁,则几乎不可能出现死锁情况。
          • 只锁定需要的内容:您应该只锁定您必须处理的资源,如果我们只对其中一个字段感兴趣,那么我们应该只锁定那个特定字段而不是完整的对象。
          • 避免无限期等待:如果两个线程使用线程连接无限期地等待对方完成,则可能会出现死锁。如果您的线程必须等待另一个线程完成,则始终最好使用您希望等待线程完成的最大时间的连接

          【讨论】:

            【解决方案10】:
            1. 避免嵌套锁。这是死锁最常见的原因。如果您已经持有另一个资源,请避免锁定另一个资源。如果您只使用一个对象锁,则几乎不可能出现死锁。

            2. 仅锁定需要的内容。如果它符合您的目的,就像锁定对象的特定字段而不是锁定整个对象。

            3. 不要无限期地等待。

            【讨论】:

              【解决方案11】:
              1. 除非需要,否则不要在多个线程上共享数据。如果创建/初始化后无法更改数据,请坚持使用最终变量。
              2. 如果您无法避免在多个线程之间共享数据,请使用细粒度的synchronized 块或Locks。
              3. 如果您仅使用 synchronized 代码块,请确保按特定顺序获取/释放锁。
              4. 寻找其他替代方案:volatile 或AtomicXXX 变量或Lock API

              相关的 SE 问题:

              Avoid synchronized(this) in Java?

              Difference between volatile and synchronized in Java

              Volatile boolean vs AtomicBoolean

              【讨论】:

                【解决方案12】:
                • 避免嵌套锁
                • 避免不必要的锁定
                • 使用线程join()

                【讨论】:

                  【解决方案13】:

                  不幸的是,存在跨多个线程的循环数据流 (A->B->C->A) 的数据模型。上面说的都不管用。 从可能有帮助的角度来看,线程之间的数据传输方法可以使用无锁缓冲输入,如 java 并发集合。数据块本身被实现为无状态的不可变对象。 GUI 系统很可能使用这种方法。这种方式似乎不需要“lockfull”等待通知方案,但是线程并发控制仍然存在,但它隐藏在集合中,并且这种方式是本地的。 这个问题真的很难避免。根源源于数学模型假设的零延迟。但在实际系统中,每个操作都需要时间,因此会产生延迟并最终导致死锁。

                  【讨论】:

                    猜你喜欢
                    • 1970-01-01
                    • 1970-01-01
                    • 2017-03-27
                    • 1970-01-01
                    • 1970-01-01
                    • 1970-01-01
                    • 2011-08-11
                    • 2016-04-01
                    相关资源
                    最近更新 更多