views:

141

answers:

5

Suppose we have a class called AccountService that manages the state of accounts.

AccountService is defined as

interface AccountService{
 public void debit(account);
 public void credit(account);
 public void transfer(Account account, Account account1);

}

Given this definition, what is the best way to implement transfer() so that you can guarantee that transfer is an atomic operation.

I'm interested in answers that reference Java 1.4 code as well as answers that might use resources from java.util.concurrent in Java 5

+10  A: 

Synchronize on both Account objects and do the transfer. Make sure you always synchronize in the same order. In order to do so, make the Accounts implement Comparable, sort the two accounts, and synchronize in that order.

If you don't order the accounts, you run the possibility of deadlock if one thread transfers from A to B and another transfers from B to A.

This exact example is discussed on page 207 of Java Concurrency in Practice, a critical book for anybody doing multi-threaded Java development. The example code is available from the publisher's website:

scompt.com
+1 for avoiding deadlock
unbeli
+1: Always locking the objects in the same order cannot be overstressed.
R. Bemrose
if performance is not critical, and there is a single AccountService, one could also simply `synchronize` the transfer-method.
aioobe
On a side note, if dealing with Java 5 and newer, it may be better to implement `Comparable<Account>` instead of `Comparable`.
R. Bemrose
None of this synchronization guarantees that if one half of the transaction fails, the other half will fail. This just deals with the synchronization issue.
Mark Peters
Please cite the authors' site http://jcip.net/, which links to more legible copies of the example code, rather than the plagiarist's.
trashgod
@trashgod: Thanks for pointing that out. I've updated my answer.
scompt.com
+2  A: 

You probably need to have a full transactions support (if it's a real application of course).

The difficulty of solution hardly depends on your environment. Describe your system in detail and we'll try to help you (what kind of application? does it use web-server? which web-server? what is used to store data? and so on)

Roman
+1 for addressing the non-synchronization issues with atomic transactions.
Mark Peters
+3  A: 

A classic example very well explained here - http://www.javaworld.com/javaworld/jw-10-2001/jw-1012-deadlock.html?page=4

Anton
excellent. will look into it.
DanielHonig
A: 

Couldn't you avoid having to synchronize using an AtomicReference<Double> for the account balance, along with get() and set()?

Pete
You'd still have to synchronize over accesses to that `AtomicReference<Double>`, as the operation `bal.set(bal.get() + creditAmt)` is not atomic: something could change the ref held by bal in between the call to `get()` and `set()`. But it's more a matter of tying the two operations (debit and credit) into one atomic operation, especially so that if one fails, the other will fail too (rollback).
Mark Peters
Making sure I understand correctly... the potential problem using Atomic variables without syncing is not what my wife does (changing the value of bal between my calls to get() and set()), but rather, what my ex-wife does (changing the reference to bal between my calls to get() and set())?
Pete
@Pete, "value" and "reference" are difficult to use in this context since you have a reference (`AtomicReference - bal`) to a reference (`Double - bal.value`) to a double value (`double - bal.value.value`). The reference `bal` would be final, so it's not an issue. The synchronization issue comes in doing the math, where you need to first 1) `get()` the Double from the AtomicReference, 2) do math on it, and 3) `set()` the value back. If you don't make 1-2-3 atomic, you could have Thread A do 1 and 2 , then Thread B do 1, then Thread A do 3, then Thread B do 2 and 3 killing A's changes.
Mark Peters
+1  A: 

If you can guarantee that all accesses are made through the transfer method, then probably the easiest approach is just to make transfer a synchronized method. This will be thread-safe because this guarantees that only one thread will be running the transfer method at any one time.

If other methods may also access the AccountService, then you might decide to have them all use a single global lock. An easy way of doing this is to surround all code that accesses the AccountService in a synchronized (X) {...} block where X is some shared / singleton object instance (that could be the AccountService instance itself). This will be thread safe because only one thread will be accessing the AccountService at any one time, even if they are in different methods.

If that still isn't sufficient, then you'll need to use more sophisticated locking approaches. One common approach would be to lock the accounts individually before you modify them... but then you must be very careful to take the locks in a consistent order (e.g. by account ID) otherwise you will run into deadlocks.

Finally if AccountService is a remote service then you are into distributed locking territory.... unless you have a PhD in computer science and years of research budget to burn you should probably avoid going there.

mikera
P.S. For simplicity and generality I'd go for the global lock until you hit a performance / scalability issue (which may be never!) and only then start to think about moving to account-level locking.
mikera
I appreciate the completeness of your answer. What you mentioned in your comment is precisely where I was headed. Once you have to get into account level locking it seems there is a need for a more elaborate scheme than simply using the 'synchronized' keyword.
DanielHonig
@DanielHonig: It's true that it's good to avoid premature optimization, but in many or most of Transaction Processing Systems (where the machine's job is basically just to do this all day long on millions of records) the designer would never even attempt to use a global mutex. They are often designed with many (many!) cores and try to work in parallel as much as possible. So it's good to know why the account-based locking is useful in this domain.
Mark Peters