【问题标题】:How to ensure locking-order to avoid deadlock?如何确保锁定顺序以避免死锁?
【发布时间】:2013-06-24 05:20:17
【问题描述】:

假设有以下 Account 类的两个对象 - account1 和 account2。并且有两个线程T1和T2。

T1 正在将金额 100 从 account1 转移到 account2,如下所示:

account1.transfer(account2, 100);

同样,T2 正在将金额 50 从 account2 转移到 account1:

account2.transfer(account1, 50);

transfer() 方法显然容易出现死锁,因为两个线程 T1 和 T2 会尝试以相反的顺序获取锁。 (线程 T1 会先尝试获取 account1 的锁,然后再获取 account2。而线程 T2 会尝试获取 account2 的锁,然后再获取 account1。)

确保始终保证锁定顺序的最佳方法是什么(在这种情况下)?

public class Account {
    private float balance;

    public class Account() {
        balance = 5000f;
    }

    private void credit(float amt) {
        balance += amt;
    }

    // To exclude noise assume the balance will never be negative
    private void debit(float amt) {
        balance -= amt;
    }

    // Deadlock prone as the locking order is not guaranteed
    public void transfer(Account acc2, float amt) {
        synchronized(this) {
            synchronized(acc2) {
                acc2.debit(amt);
                this.credit(amt);
            }
        }
    }
}

【问题讨论】:

标签: multithreading locking deadlock


【解决方案1】:

我只会让一个线程访问“帐户”数据。任何其他想要转移资金的线程都必须向其排队一个“transferRequest”对象,该对象包含帐户 ID、要转移的金额、异常/errorMessage 字段和回调/事件,以 transferRequest 作为参数,用于线程尝试交易时调用。

然后将传输全部序列化,唯一的锁在队列中,因此不可能出现死锁。

我讨厌多把锁,不管顺序是否正确。

【讨论】:

  • 感谢马丁的回复。基本上,您正在提议基于事件的机制来实现这一点 - 同意您的看法。但是,我发布这个问题的目的是要知道“应该如何确保锁定顺序?”
  • 是的 - 我不确定我的回复应该是答案还是评论,但决定回答。我可以争辩说'我的解决方案通过将锁的数量减少到一个来确保锁定的顺序,而与帐户或线程的数量无关。如果没有排序,则排序不能不正确':) 高效的多线程设计的一部分不是解决问题,而是让问题完全消失,因此不必解决它们。这当然适用于多个锁。
【解决方案2】:

您可以自己实现同步块的排序。在创建时为每个帐户创建一个唯一 ID,并按排序顺序使用同步:

class Account {

  private float balance;
  private final int id;
  private static AtomicInteger idGen = new AtomicInteger(0);

  public Account() {
    id = idGen.incrementAndGet();
    balance = 5000f;
  }

  private void credit(float amt) {
    balance += amt;
  }

  // To exclude noise assume the balance will never be negative
  private void debit(float amt) {
    balance -= amt;
  }

  // Deadlock prone as the locking order is not guaranteed
  public void transfer(Account acc2, float amt) {
    Account first = this.id > acc2.id ? acc2 : this;
    Account second = this.id > acc2.id ? this : acc2;

    synchronized (first) {
      synchronized (second) {
        acc2.debit(amt);
        this.credit(amt);
      }
    }

  }
}

但这种方法只有在您提前知道要锁定的所有帐户时才可用。


编辑: 我将尝试澄清有关提前了解所有锁的部分。

在像这样的简单示例中,很容易收集所有需要的锁,对它们进行排序,然后以正确的顺序锁定它们。当您的代码变得越来越复杂并且您尝试使用抽象来保持代码可读性时,问题就开始了。锁排序概念有点反对抽象。当您调用一些封装的未知代码(可能会尝试获取更多锁或调用其他代码)时,您将无法再确保正确的锁顺序。

【讨论】:

  • 这看起来是一个很好且简单的解决方案 - 谢谢!但是,我不明白为什么你说这种方法只有在所有要锁定的帐户都事先知道的情况下才可用?能详细点吗?
【解决方案3】:

您可以定义一个shared mutex 来锁定,以便当任何线程想要进行事务时,它会尝试获取该对象而不是帐户。如果一个线程锁定了这个共享对象,那么您可以进行事务。事务完成后,它可以释放锁,以便另一个线程可以再次获取该对象。

【讨论】:

  • 这是一个解决方案 - 谢谢。但是,如果有 4 个帐户 a1 到 a4 和 4 个线程 T1 到 T4。 T1 和 T2 与 a1 和 a2 一起工作。 T3 和 T4 与 a3 和 a4 一起使用。我希望(T1 & T2 组)和(T3 & T4 组)同时工作,即这两组操作不需要等待锁定。
  • @Learner,这绝对不是问题,您只需要为帐户对的每个组合定义一个互斥对象即可。对于 4 个帐户,您可以在一个向量中使用 6 个单独的互斥锁。
  • 所以实现变得相当复杂,以概括互斥体的排列和组合。另外,如果有 100 万个帐户,我无法想象需要多少互斥锁?!我们可能会延迟初始化互斥锁,但这会带来新的复杂性。
  • @Learner,那么您可以将互斥锁保留为帐户的成员,以便当一个帐户想要连接另一个帐户时,它只是尝试获取它。互斥锁的总数将等于帐户总数
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2013-06-07
  • 2012-08-14
  • 1970-01-01
  • 2017-12-17
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多