死锁和同步方法

5

我在Stack Overflow上找到了一段代码,我认为它与我遇到的问题非常相似,但我仍然不明白为什么会进入死锁。这个例子来自于Java中的死锁检测

Class A
{
  synchronized void methodA(B b)
  {
    b.last();
  }

  synchronized void last()
  {
    System.out.println(“ Inside A.last()”);
  }
}

Class B
{
  synchronized void methodB(A a)
  {
    a.last();
  }

  synchronized void last()
  {
    System.out.println(“ Inside B.last()”);
  }
}

Class Deadlock implements Runnable 
{
  A a = new A(); 
  B b = new B();

  // Constructor
  Deadlock()
  {
    Thread t = new Thread(this); 
    t.start();
    a.methodA(b);
  }

  public void run()
  {
    b.methodB(a);
  }

  public static void main(String args[] )
  {
    new Deadlock();
  }
}

在这种情况下,当调用Deadlock()构造函数时,它会作为线程启动。此时将调用run()方法。它将调用b.methodB(a),然后调用a.last()只打印一条语句。同时,a.methodA(b)会调用b.last()。没有任何对象的交叉依赖,并且它们也不会同时执行一个方法。即使它们同时执行,使用synchronized关键字也会将它们排队,不是吗?但是为什么有时候会进入死锁呢?并非每次都会发生死锁,这相当难以预测。是什么导致它进入死锁,有哪些解决方法?

3个回答

23

这两个语句的执行可能会交织在一起:

Thread 1:  a.methodA(b);    //inside the constructor
Thread 2:  b.methodB(a);    //inside run()

要执行a.methodA(),线程1需要获取A对象的锁。

要执行b.methodB(),线程2需要获取B对象的锁。

然后,线程1的methodA()要调用b实例上的同步方法,它需要获取由线程2持有的b的锁,这会导致线程1等待该锁被释放。

线程2的methodB()要能够调用a实例上的同步方法,则需要获得由线程1持有的a锁,这也会导致线程2等待。

由于每个线程都持有另一个线程想要的锁,将会发生死锁,即两个线程都无法获取所需的锁,并且它们也不会释放它们持有的锁。

需要理解的是,这段代码不会在每次运行时都产生死锁 - 只有在按照一定顺序执行四个关键步骤(线程1持有A的锁并尝试获取B,而线程2则持有B的锁并尝试获取A)时才会产生死锁。反复运行此代码,这种顺序肯定会发生。


2
这是答案,但我们什么时候才能在SO上绘制图表呢? :-) - Alvin
@Alvin,你可以从http://www.websequencediagrams.com/或http://yuml.me/嵌入图像。 - matt b
@matt 是的,那也可以,但不如直接在SO上绘制好看。 - Alvin
在Java中有没有办法可以防止这个问题?我正在思考如何避免这个死锁问题,但似乎非常困惑,因为它们都会偶尔陷入彼此自动等待的情况。谢谢! - Carven
@xEnOn:避免这种死锁很容易。如果您需要在程序的不同部分锁定相同的两个或多个对象,请始终以相同的顺序锁定它们。 - Jake T.
显示剩余2条评论

2

synchronized 在方法或代码块执行前会对 对象 加锁。由于它锁定整个对象,因此它是一个不太优雅的工具,有时看起来很容易使用,但会导致死锁,即没有实际竞争数据被读取或写入时发生。

a.method(b) 锁定 a 对象,b.method(a) 锁定 b 对象。然而,两个执行线程都在等待另一个对象释放其锁定状态,因此无法继续调用 b.last()a.last() 方法。


但是如果我不使用synchronized,该方法可能只会运行一半,然后被其他线程中断。但是使用synchronized会导致出现锁定问题。通常如何解决这样的问题?谢谢。 - Carven
1
@Xenon,学习如何正确编写并发程序是一个广阔的研究领域;我听到的最好建议是“锁定数据,而不是代码”。Java的synchronized使锁定代码变得容易,但锁定特定的数据对象是更有用的工具,从长远来看。通常也更容易理解对于_数据_的锁定,因此您可以强制执行锁定层次结构以防止死锁。 - sarnold

0

调用methodA会(有效地)锁定(a),锁定(b)。如果任务此时切换并尝试methodB,则会立即触发锁定(b)。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接