嵌套同步块

6

让我们假设我有下面这些类:

public class Service {
    public void transferMoney(Account fromAcct, Account toAcct, int amount) {
      synchronized (fromAcct) {
        synchronized (toAccount) { // could we use here only one synchronized block?
            fromAcct.credit(amount);
            toAccount.debit(amount);
        }
      }
    }
}

class Account {
  private int amount = 0;

  public void credit(int sum) {
    amount = amount + sum;
  }

  public void debit(int sum) {
    amount = amount - sum;
  }
}

例如,我知道我们只能在transferMoney方法中更改fromAccttoAcct对象的状态。因此,我们能否使用一个synchronized块重写我们的方法?
public class Service {
 private final Object mux = new Object();

 public void transferMoney(Account fromAcct, Account toAcct, int amount) {
      synchronized(mux) {
        fromAcct.credit(amount);
        toAcct.debit(amount);
      }
 }
}

3
A) 不行。 B) 那样不安全。 - SLaks
如果您有两个“Service”实例,会怎样呢? - user253751
如果您正在尝试使2个操作(信用+借记)具有事务性,则同步不是实现的方式。如果您的目标是确保在多个线程同时在同一帐户中移动资金时不会破坏帐户余额,则应从Account类内部进行同步。 - sstan
@SLaks,您能否解释一下您的A)答案?我同意我们可能会在这里遇到死锁问题。 - Iurii
@lurii:不行。如果在借方步骤中发生异常,你认为会发生什么?那么你最终会得到一笔贷方而没有相应的借方,同步根本无法帮助你保护自己免受此类情况的影响。你需要的是一个数据库来为你管理事务。 - sstan
显示剩余4条评论
2个回答

5
除非你有一种我无法理解的非常独特和特殊的需求,否则我的看法是,你的目标应该是保护账户余额,防止多个线程同时尝试向账户存入或取出而导致账户余额出现错误。
为了实现这个目标,应该这样做:
public class Service {
    public void transferMoney(Account fromAcct, Account toAcct, int amount) {
        fromAcct.credit(amount);
        toAccount.debit(amount);
    }
}

class Account {
    private final object syncObject = new Object();
    private int amount = 0;

    public void credit(int sum) {
        synchronized(syncObject) {
            amount = amount + sum;
        }
    }

    public void debit(int sum) {
        synchronized(syncObject) {
            amount = amount - sum;
        }
    }
}

如果您在资金转移过程中的目标是始终确保信用和借记操作作为一个事务或原子性发生,那么使用同步不是正确的方法。即使在同步块中,如果出现异常,那么您失去了两个操作同时发生的保证。
自己实现事务是一个非常复杂的话题,这就是为什么我们通常使用数据库来完成这项工作。
编辑:OP问:我的示例(一个同步块MUX)与你的Account类中的同步有什么区别?
这是一个公平的问题。有一些区别。但我认为主要的区别是,具有讽刺意味的是,您的示例过度同步。换句话说,即使您现在使用单个同步块,您的性能实际上可能会更差。
考虑以下示例: 您有4个不同的银行账户:让我们称它们为A、B、C、D。 现在您有2笔在完全相同时间启动的汇款:
从帐户A到帐户B的汇款。 从帐户C到帐户D的汇款。
我认为您会同意,因为2笔汇款正在完全不同的帐户上进行,所以执行两笔汇款不会有任何损害(没有腐败风险),对吧?
然而,在您的示例中,汇款只能一个接一个地执行。在我的示例中,两笔汇款同时发生,但也是安全的。只有当两笔汇款尝试“触及”相同的帐户时,我才会阻塞。
现在想象一下,如果您使用此代码处理数百、数千或更多并发汇款。那么毫无疑问,我的示例将比您的示例表现得更好,同时仍然保持线程安全,并保护账户余额的正确性。
实际上,我的代码版本在概念上的行为更像您最初的2个同步块代码。除了以下改进:
修复了潜在的死锁场景。 意图更清晰。 提供更好的封装。(这意味着即使在transferMoney方法之外的某些其他代码尝试借记或信用某些金额,我仍将保留线程安全性,而您则不会。我知道您说这不是您的情况,但是对于我的版本,设计绝对保证了它)

先贷后借? :) 不过混淆信用和借记是 OP 的错。 - ZhongYu
@SamStanojevic 感谢您的回复!抱歉我没有和您表达清楚。是的,我的目标是保护账户余额不受多个线程执行的破坏。因此,正如您早先提到的那样,我应该在Account方法中使用synchronized来实现这个目的。但是,如果我在transferMoney方法中使用synchronized,那么它是否可以保护我的代码免受多个线程执行的影响呢? - Iurii
@lurii:是的,如果你在transferMoney方法中使用synchronized,那么它将保护你的代码不被多个线程同时执行。但它不会给你任何事务保证。非常重要的是,你要理解线程安全和事务之间的根本区别。它们是两个完全不同的东西,也是通过完全不同的方式实现的。 - sstan
@SamStanojevic 谢谢你的澄清。我会采用你的答案,只有一个问题。我的示例(一个同步块mux)和你在Account类中的同步有什么区别?很抱歉问这些基本的问题,但对我来说这真的很重要。 - Iurii
@lurii:我编辑了我的帖子来回答你的后续问题。在评论中我无法给你一个恰当的回应。 - sstan
@SamStanojevic 哦,我明白了...非常感谢! - Iurii

2

看起来您希望通过同步实现事务。它们之间没有任何共同点。事务提供操作的完整性 - 执行所有操作或全部回滚。同步确保数据只能从一个线程更改。例如,事务确保如果您从一个账户取钱而不将其存入另一个账户,则第一次行动是撤消 - 钱没有被取出。同步检查如果两个不同的人在完全相同的时刻向银行存2便士,那么银行将有4便士而不仅仅是2,因为您的程序根据以前的值向同一个账户添加资金。


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