假设以下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);
}
}
}
}
答案 0 :(得分:1)
我只给一个线程访问'accounts'数据。任何其他想要转移资金的线程都必须将'transferRequest'对象排入其中,该对象包含帐户ID,要传输的异常/ errorMessage字段和回调/事件的数量,以及transferRequest作为参数,用于该线程在尝试交易时调用。
然后传输完全序列化,唯一的锁在队列中,因此无法进行死锁。
我讨厌多个锁,正确订购或没有。
答案 1 :(得分:1)
您可以自己实现同步块的排序。在创建时为每个帐户创建唯一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);
}
}
}
}
但只有当您知道要提前锁定的所有帐户时,此方法才可用。
编辑: 我将尝试澄清关于提前了解所有锁的部分。
在这样一个简单的例子中,很容易收集所有需要的锁,对它们进行排序,然后按正确的顺序锁定它们。当您的代码变得越来越复杂并且您尝试使用抽象来保持代码可读时,问题就开始了。锁订购概念类型的aginst抽象。当您调用某些封装的未知代码(migth尝试获取更多锁或调用其他代码)时,您无法再确保正确的锁定顺序。
答案 2 :(得分:0)
您可以定义要锁定的shared mutex
,以便当任何线程想要进行交易时,它会尝试获取该objact而不是帐户。如果线程锁定到此共享对象,则可以进行事务。当事务完成时,它可以释放锁,以便另一个线程可以再次获取该对象。