48

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

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

4

13 回答 13

39

我脑海中的一些快速提示

  • 不要使用多个线程(例如 Swing 所做的那样,强制所有事情都在 EDT 中完成)
  • 不要同时持有几把锁。如果这样做,请始终以相同的顺序获取锁
  • 持有锁时不要执行外来代码
  • 使用可中断锁
于 2013-05-27T21:51:59.593 回答
15

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

于 2013-05-28T03:57:43.607 回答
12
  1. 通过使用无锁数据结构避免锁(例如使用 aConcurrentLinkedQueue而不是 synchronized ArrayList
  2. 始终以相同的顺序获取锁,例如为每个锁分配一个唯一的数值,并在获取数值较高的锁之前先获取数值较小的锁
  3. 在超时时间后释放你的锁(从技术上讲,这并不能防止死锁,它只是有助于在它们发生后解决它们)
于 2013-05-27T21:51:08.850 回答
10

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

于 2013-05-27T22:40:45.723 回答
9
  1. 不要使用锁。
  2. 如果必须,请将锁放在本地。全局锁可能非常棘手。
  3. 握住锁时尽量少做。
  4. 使用带仅锁定数据段
  5. 首选不可变类型。很多时候,这意味着复制数据而不是共享数据。
  6. 请改用比较和设置 (CAS) 机制,例如参见AtomicReference 。
于 2013-05-27T22:30:05.130 回答
5

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

于 2013-05-28T00:29:09.147 回答
5

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

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

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

于 2013-05-27T21:51:01.823 回答
4

布尔标志示例

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

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 永远是 true

换句话说,Thread1 将始终从设置为flag的 中读取值。cachetrue


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

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

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

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)
     {
      //...
     }
   }
}

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

于 2013-05-27T21:54:12.013 回答
2

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

如何在 Java 中检测死锁

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

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

如何避免java中的死锁

这些是我们可以避免大多数死锁情况的一些指导方针。

  • 通过打破循环等待条件来避免死锁:为了做到这一点,您可以在代码中进行安排,以强制获取和释放锁的顺序。如果锁将以一致的顺序获取并以相反的顺序释放,则不会出现一个线程持有由另一个线程获取的锁的情况,反之亦然。
  • 避免嵌套锁:这是死锁最常见的原因,如果您已经持有另一个资源,请避免锁定另一个资源。如果您只使用一个对象锁,几乎不可能出现死锁情况。
  • Lock Only What is required:你应该只对你必须处理的资源获取锁,如果我们只对它的一个字段感兴趣,那么我们应该只锁定那个特定的字段而不是完整的对象
  • 避免无限期等待:如果两个线程使用线程连接无限期地等待对方完成,则可能会出现死锁。如果您的线程必须等待另一个线程完成,则最好使用 join with maximum time you want to wait for the thread to finish
于 2019-01-26T21:19:49.847 回答
1
  1. 除非需要,否则不要在多个线程上共享数据。如果创建/初始化后无法更改数据,请坚持使用最终变量。
  2. 如果您无法避免在多个线程之间共享数据,请使用粒度synchronized块或Lock
  3. 如果您只使用synchronized代码块,请确保以特定顺序获取/释放锁。
  4. 寻找其他替代方案:volatile 或AtomicXXX变量或LockAPI

相关的 SE 问题:

在 Java 中避免同步(this)?

Java 中 volatile 和 synchronized 的区别

易失性布尔值与原子布尔值

于 2016-05-18T13:00:57.080 回答
1
  • 避免嵌套锁
  • 避免不必要的锁
  • 使用线程连接()
于 2018-04-24T23:14:22.093 回答
1
  1. 避免嵌套锁。这是死锁最常见的原因。如果您已经持有另一个资源,请避免锁定另一个资源。如果您只使用一个对象锁,几乎不可能出现死锁。

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

  3. 不要无限期地等待。

于 2016-05-14T03:29:07.253 回答
0

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

于 2021-08-21T18:10:45.807 回答