Java synchronisation: atomically moving money across account pairs?
A simple solution could be to use a lock per account, but to avoid deadlock you have to acquire locks in the same order always. So, you could have a final account ID, and acquire the lock of the account with a less id first:
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);
}
}
}
One way to do this is to have a transaction log. Before moving the money, you'll need to write to the transaction log of each account what you intend to do. The log should contain: the amount of money that's taken in/out of the account, and an lock which is shared between the log pair.
Initially the lock should be in a blocked state. You created the log pair, one with amount of X and the other with amount of -X, and both shares a lock. Then deliver the log entry to the inbox of the respective accounts, the account from which money is taken out should reserve that amount. Once you've confirmed that they're delivered safely, then release the lock. The moment the lock is released you're at a point if no return. The accounts then should resolve themselves.
If either of the party want to fail the transaction at any time before the lock is released, then simply remove the logs and return the reserved amount to the main balance.
This approach may be a bit heavy, but it would also work in a distributed scenario where the accounts actually are in different machines, and the inboxes actually would have to be persisted, to ensure money never get lost if any of the machine crashes/goes offline unexpectedly. Its general technique is called two phase locking.
I would propose to create a method Account.withdraw(amount) which throws an exception if it doesn't have sufficient funds. This method needs to be synchronized on the account itself.
Edit:
There also needs to be a Account.deposit(amount) method which is synchronized on the receiving account instance.
Basically this will result in a lock of the first account while withdrawing and then another lock on the receiving account while depositing. So two locks but not at the same time.
Code sample: Assumes that withdraw/deposit are synchronized and return boolean success status rather than throw exception.
public boolean transfer(Account from, Account to, BigDecimal amount) {
boolean success = false;
boolean withdrawn = false;
try {
if (from.withdraw(amount)) {
withdrawn = true;
if (to.deposit(amount)) {
success = true;
}
}
} finally {
if (withdrawn && !success) {
from.deposit(amount);
}
}
return success;
}