Java死锁代码解释

4

请问有人能解释为什么以下代码会导致死锁吗? 我的理解是当阿尔方斯(线程)运行时,它会因为调用bow()方法而获得friend对象的锁,但是为什么加斯顿(另一个线程)能够在阿尔方斯还没有完成/释放friend对象的锁的情况下获得同一friend对象的锁呢?

public class Deadlock {
static class Friend {
    private final String name;
    public Friend(String name) {
        this.name = name;
    }
    public String getName() {
        return this.name;
    }
    public synchronized void bow(Friend bower) {
        System.out.println("invoked by " + name);
        System.out.format("%s: %s"
            + "  has bowed to me!%n", 
            this.name, bower.getName());
        bower.bowBack(this);
        System.out.printf("finished by " + name);
    }
    public synchronized void bowBack(Friend bower) {
        System.out.format("%s: %s"
            + " has bowed back to me!%n",
            this.name, bower.getName());
        System.out.println("exiting bowBack()");
    }
}

public static void main(String[] args) throws InterruptedException {
    final Friend alphonse =
        new Friend("Alphonse");
    final Friend gaston =
        new Friend("Gaston");
    new Thread(new Runnable() {
        public void run() { alphonse.bow(gaston); }
    }).start();
    new Thread(new Runnable() {
        public void run() { gaston.bow(alphonse); }
    }).start();
}

}


1
你正在对类的实例进行同步。你应该在类中声明一个static final字段进行同步,这将使得等待所有类的实例。 - Luiggi Mendoza
@Luiggi Mendoza 你的意思是bower.bowBack(this)吗? - sol4me
不,我的意思是 public synchronized void yourMethod - Luiggi Mendoza
1
@jtahlborn,至少我也学到了一些东西。 - Christian
这有点像作业,但是是用来自我教学的 :P - Luiggi Mendoza
显示剩余2条评论
4个回答

6
  1. 线程1:alphonse.bow()。要进入此方法,线程1需要获取alphonse的锁,因为bow()方法是同步的。
  2. 线程2:gaston.bow()。要进入此方法,线程2需要获取gaston的锁,因为bow()方法是同步的。
  3. 线程1:gaston.bowBack()。要进入此方法,线程1需要获取gaston的锁,因为bowBack()方法是同步的。它会等待直到线程2释放gaston的锁。
  4. 线程2:alphonse.bowBack()。要进入此方法,线程2需要获取alphonse的锁,因为bowBack()方法是同步的。它会等待直到线程1释放alphonse的锁。

这两个线程最终互相等待。这是一种死锁状态。


3

想象两个线程并行执行:

thread1                               thread2
-----------------------------------   -----------------------------------
enter alphonse.bow(gaston)            enter gaston.bow(alphonse)
  - acquire lock on alphonse            - acquire lock on gaston

gaston.bowBack(alphonse)              alphonse.bowBack(gaston)
  - try to acquire lock on gaston;      - try to acquire lock on alphonse;
    blocked because of gaston.bow()       blocked because of alphonse.bow()

此时,两个线程都在等待另一个释放锁,因此都无法完成同步方法以释放锁(因为它们正在等待另一个线程)。


2
您有一个循环锁链: alphonse.bow获取alphonse上的锁,然后尝试获取可能已被占用的gaston上的锁。 gaston.bow获取gaston上的锁,然后尝试获取已被占用的alphonse上的锁。
为了分析/可视化这个过程,请绘制线程图(例如两个圆圈)和资源图(例如两个矩形)。当一个线程请求资源时,请从线程到资源绘制一条箭头。当资源被授予时,请从该资源到所有者线程绘制一条箭头。 只要没有出现循环,一切都很好。 如果您最终遇到循环,则需要牺牲该循环中的某些内容以打破循环。

0

这是典型的死锁场景:您有N个对象,每个对象都有自己的锁,并且您以随机顺序获取多个这些锁。

问题发生在两个线程试图"鞠躬"互相时。每个线程获得自己人物的锁(Alphonse和Gaston),然后尝试在仍然持有自己人物锁的同时获得朋友的锁。

可以通过不同的方法来解决这个问题。

最简单的方法是通常使用全局锁(例如,在Friend类中定义的静态锁定对象)。这通常只允许一个线程一次执行弯腰/弯腰回归序列。有时候这很好,有时候它可能成为性能瓶颈。

更复杂的解决方案是确保锁以确定的顺序获取,例如确保在获取Gaston锁之前始终获取Alphonse锁。为此,您可以向要锁定的对象引入排序标准,为简单起见,假设我们可以依赖于朋友的名称是唯一的。

这使得实现稍微复杂了一些,但是相比于全局锁,它允许更多的并发性,同时避免了死锁。方法不再盲目地在"self"上进行同步,而是根据优先级顺序决定在谁身上进行同步:

public void bow(Friend bower) {
    // determine locking order
    Friend first, second;
    if (getName().compareTo(bower.getName()) > 0) {
        first = this;
        second = bower;
    } else {
        first = bower;
        second = this;
    }
    synchronized (first) {
        synchronized (second) {
             // perform bow normally
        }
    }
}

这是有效的,因为无论阿方斯向加斯顿鞠躬还是反之,他们都会以相同的顺序获取锁定。

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