Java同步:在账户对之间原子转移资金?

2022-08-31 20:50:56

如何从一个账户转账到另一个原子账户?对于类:

public class Account {
    public Account(BigDecimal initialAmount) {...}
    public BigDecimal getAmount() {...}
    public void setAmount(BigDecimal amount) {...}
}

我希望以下伪代码:

public boolean transfer(Account from, Account to, BigDecimal amount) {
    BigDecimal fromValue = from.getAmount();
    if (amount.compareTo(fromValue) < 0)
         return false;
    BigDecimal toValue = to.getAmount();
    from.setAmount(fromValue.add(amount.negate()));
    to.setAmount(toValue.add(amount));
    return true;
}

在单线程(或顺序)环境中安全地更新帐户。

在多线程/并发环境的情况下,我看到危险情况:

acc1 --> acc2  ||  acc2 --> acc1
acc1 --> acc2  ||  acc2 --> acc3  ||  acc3 --> acc1
...

最简单的解决方案是阻止共享对象,但对于以下情况,这将是低效的:

acc1 --> acc2  ||  acc3 --> acc4  and  acc1 != acc3 and acc2 != acc4

我希望独立传输是并行执行的。

更新似乎建议的解决方案:

synchronize (acc1) {
   synchronize (acc2) {
     ....
   }
}

导致死锁,因为2个锁依次获得...

UPDATE 2 “在多线程环境中安全地更新帐户”到底是什么意思?唯一担心的是账户最终不会有零资金还是有其他问题?

如果 and 和我期望一致性:有价值,但没有或 .总数应为 5,而不是 1+3=4 或 4+3=7。acc1(2); acc2(3)acc1 --1--> acc2acc2 --2--> acc1(acc1, acc2)(3, 2)(4, 2)(3, 4)

您一次需要多少并发事务?1000-10000 - 因此锁定共享对象效率不高。


答案 1

一个简单的解决方案可能是为每个帐户使用一个锁,但为了避免死锁,您必须始终以相同的顺序获取锁。因此,您可以拥有最终的帐户ID,并首先使用较少的ID获取帐户的锁定:

public void transfer(Account acc1, Account acc2, BigDecimal value) {
    Object lock1 = acc1.ID < acc2.ID ? acc1.LOCK : acc2.LOCK;
    Object lock2 = acc1.ID < acc2.ID ? acc2.LOCK : acc1.LOCK;
    synchronized (lock1) {
       synchronized (lock2) {
          acc1.widrawal(value);
          acc2.send(value);
       }
    }
}

答案 2

执行此操作的一种方法是拥有事务日志。在转移资金之前,您需要在每个帐户的交易日志中写下您打算做的事情。日志应包含:从帐户中取出/取出的金额,以及在日志对之间共享的锁。

最初,锁应处于阻塞状态。您创建了日志对,一个数量为 X,另一个数量为 -X,并且两者都共享一个锁。然后将日志条目发送到相应帐户的收件箱,从中取出资金的帐户应保留该金额。确认它们已安全交付后,请松开锁。当锁被释放的那一刻,如果没有返回,你就到了一个点。然后,帐户应自行解析。

如果任何一方希望在释放锁定之前的任何时间使交易失败,则只需删除日志并将预留金额返回给主余额即可。

这种方法可能有点重,但它也适用于分布式场景,其中帐户实际上位于不同的计算机中,并且实际上必须保留收件箱,以确保在任何计算机意外崩溃/脱机时都不会丢失资金。它的一般技术称为两相锁定。