1

I am trying to implement a banking system where I have a set of accounts. There are multiple threds trying to transfer money between accounts, while one thread continuosuly (or rather, at random times) tries to sum up the total money in the bank (Sum of all accounts balance).

The way to solve that sounded obvious at first; Using ReentrantReadWriteLocks with readLock for the threads doing the transactions and writeLock for the thread doing the summation. However after implementing it that way (see code below), I saw a huge drop in performance / "transaction-throughput" even versus doing transactions with only one thread.

  • Code for above mentioned implementation:

    public class Account implements Compareable<Account>{
       private int id;
       private int balance;
    
       public Account(int id){
          this.id = id;
          this.balance = 0;
       }
    
       public synchronized int getBalance(){ return balance; }
    
       public synchronized setBalance(int balance){
          if(balance < 0){ throw new IllegalArgumentException("Negative balance"); }
          this.balance = balance;
       }
    
       public int getId(){ return this.id; }
    
       // To sort a collection of Accounts.
       public int compareTo(Account other){
          return (id < other.getId() ? -1 : (id == other.getId() ? 0 : 1));
       }
    }
    
    public class BankingSystem {
       protected List<Account> accounts;
       protected ReadWriteLock lock = new ReentrantReadWriteLock(); // !!
    
       public boolean transfer(Account from, Account to, int amount){
          if(from.getId() != to.getId()){
             synchronized(from){
                if(from.getBalance() < amount) return false;
                lock.readLock().lock(); // !!
                from.setBalance(from.getBalance() - amount);
             }
             synchronized(to){ 
                to.setBalance(to.getBalance() + amount);
                lock.readLock().unlock(); // !! 
             }
          }
          return true;
       }
    
       // Rest of class..
    }
    

Note that this is without even using the summation method yet, so no writeLock is ever being aquired. If I only delet the lines marked with a // !! and also do not call the summation method, suddenly the "transfer throughput" using multiple threads is a lot higher than when using a single thread, as is the goal.

My question now is, why does that simple introduction of a readWriteLock slow down the entire thing that much, if I never try to aquire a writeLock, and what I did wrong here, because I can't manage to find the problem.

  • Sidenote: I had asked a question regarding this problem already here, but managed to ask the wrong question. I did however, get an amazing answer for that question I asked. I decided to not reduce question quality immensly, and to keep that great answer alive for people in need for help regardin that matter, I would not edit the question (yet again). Instead I opened this question, in the strong belief that this is NOT a duplicate, but instead an entirely different matter.
Community
  • 1
  • 1
Kugelblitz
  • 582
  • 5
  • 24

3 Answers3

2

Locking is expensive but in your case, I assume there might be some kind of "almost deadlock" when you run the tests: If some thread is in the synchronized(from){} block of your code and another thread wants to unlock the from instance in it's synchronized(to){} block, then it won't be able to: The first synchronized will prevent thread #2 from entering the synchronized(to){} block and hence the lock won't be released very quickly.

That could lead to a lot of threads hanging in the queue of the lock which makes it slow to get/release locks.

Some more notes: Your code will cause problems when the second part (to.setBalance(to.getBalance() + amount);) isn't executed for some reason (exception, deadlock). You need to find out a way to create a transaction around the two operations to make sure they are either executed both or none are executed.

A good way to do this is to create a Balance value object. In your code, you can create two new ones, update both balances and then just call the two setters - since setters can't fail, either both balances will be updated or the code will fail before any setters could be called.

Aaron Digulla
  • 321,842
  • 108
  • 597
  • 820
  • The lock is a shared lock. As long as there is no write lock held, there can’t be any deadlock. – Holger Apr 15 '15 at 09:57
  • @Holger: `synchronized` is a write lock. – Aaron Digulla Apr 15 '15 at 10:00
  • I can see the suggestion you make with the two synchronized statements blocking each other, thank you for that hint! However, the problem can't really be there, since if I remove the ReadWriteLock, I have no performance issues.. – Kugelblitz Apr 15 '15 at 10:01
  • @Phil: It should also go away if you remove the `synchronized` blocks (but then, you'll get bugs with the balance). The thing is that the `synchronized` blocks interfere with the lock. Try to move the lock outside. – Aaron Digulla Apr 15 '15 at 10:02
  • But there are no nested `synchronized` blocks, there is no loop and no blocking operation involved, so your explanation that there is any lock not timely released is simply wrong. – Holger Apr 15 '15 at 10:03
  • @Holger: You didn't understand what I wrote. I quoted the word "deadlock". It's not really a deadlock. But imagine what happens when several threads can't enter the `synchronized(to){}` block. – Aaron Digulla Apr 15 '15 at 10:05
  • Oh, and there was an "ever" too much. The synchronized blocks might simply throw sticks in the path of the unlock. – Aaron Digulla Apr 15 '15 at 10:06
  • @AaronDigulla So you are suggesting I should simply move the lock.readLock().lock() and unlock respectively outside the synchronized blocks? – Kugelblitz Apr 15 '15 at 10:08
  • @Aaron Digulla: it is right that the `synchronized` blocks may delay the unlock, but as my *first* comment already hinted, it’s a *shared* lock so delayed unlocking does *not* explain any slowdown as the `lock` operation succeds, regardless of how many `unlock`s are pending – Holger Apr 15 '15 at 10:12
  • @Holger: http://grepcode.com/file/repository.grepcode.com/java/root/jdk/openjdk/6-b14/java/util/concurrent/locks/ReentrantReadWriteLock.java#399 – Aaron Digulla Apr 15 '15 at 11:24
  • @Aaron Digulla: the OP already confirmed that using a lock free algorithm without `synchronized` blocks has exactly the same performance characteristics. It’s right that this code you have linked can become a bottleneck if there’s a large amount of threads but that has nothing to do with the interaction with `synchronized`. – Holger Apr 15 '15 at 11:30
2

First of all, it’s correct to put the update into its own synchronized block, even if the getter and setter are synchronized on its own, so you avoid the check-then-act anti-pattern.

However, from a performance standpoint it’s not optimal as you acquire the same lock three times (four times for the from account). The JVM, or the HotSpot optimizer, knows the synchronization primitives and is able to optimize such patterns of nested synchronization, but (now we have to guess a bit) if you acquire another lock in-between, it might prevent these optimizations.

As already suggested at the other question, you may turn to a lock free update, but of course you have to fully understand it. The lock free updates are centered around one special operation, compareAndSet which performs an update only if the variable has the expected old value, in other words, no concurrent update has been performed in-between, whereas checking and updating is performed as one atomic operation. And that operation is not implemented using synchronized but utilizing a dedicated CPU instruction directly.

The use pattern is always like

  1. read the current value
  2. calculate the new value (or reject the update)
  3. attempt to perform an update which will succeed if the current value is still the same

The drawback is that the update might fail which requires repeating the three steps but it’s acceptable if the computation is not too heavy and since a failed update indicates that another thread must have succeed with its update in-between, there will always be a progress.

This led to the example code for an account:

static void safeWithdraw(AtomicInteger account, int amount) {
    for(;;) { // a loop as we might have to repeat the steps
        int current=account.get(); // 1. read the current value
        if(amount>current) throw new IllegalStateException();// 2. possibly reject
        int newValue=current-amount; // 2. calculate new value
        // 3. update if current value didn’t change
        if(account.compareAndSet(current, newValue))
            return; // exit on success
    }
}

So, for supporting lock-free access it’s never sufficient to provide getBalance and setBalance operations as every attempt to compose an operation out of get and set operations without locking will fail. You have three options:

  1. Provide every supported update operation as a dedicated method like the safeWithdraw method
  2. Provide a compareAndSet method to allow callers to compose their own update operations using that method
  3. Provide an update method that takes an update function as paramater, like the AtomicInteger does in Java 8; Of course, this comes especially handy when using Java 8 where you can use lambda expressions to implement the actual update function.

Note that AtomicInteger itself uses all options. There are dedicated update methods for common operations like increment and there’s the compareAndSet method allowing composing arbitrary update operations.

Holger
  • 285,553
  • 42
  • 434
  • 765
  • Thanks a lot for this explanation, I'll try to change my implementation accordingly now, and see where that gets me! – Kugelblitz Apr 15 '15 at 10:15
  • I changed the implementation of class `Account` to fit the requirements now, with a `safeDeposit` and a `safeWithdraw` method (unsynchronized, but atomic). And that perfectly works without the `ReadWriteLocks`. (The `transfer` method now also contains no synchronized statements any more.). However, as soon as I try to lock / unlock the `ReadWriteLock` again, the performance drop reappears. It seems like that has to do more with the use of the `ReadWriteLock` in the first place, instead of an implementation problem, maybe? – Kugelblitz Apr 15 '15 at 10:33
  • What happens if you profile now? Unlike your original code, the threads can’t be blocked by `synchronized` now… – Holger Apr 15 '15 at 10:37
  • That's right, they can't, and they aren't. If I profile, all seems normal, no blocking each other out, but it just takes a riddiculous amount of time longer with the introduced readWriteLock. That's why I said, I'm starting to feel like the problem lies more in the choice of lock than it's implementation, which is also hard to imagine though.. – Kugelblitz Apr 15 '15 at 10:40
  • Not yet, I have only tried running / compiling with Java 8 up to now. Should I try Java 7? – Kugelblitz Apr 15 '15 at 11:03
  • I think it’s worth to check whether other versions have a different behavior. After thinking about it, if you have really lots of threads which usually work on different accounts, the updating of a single lock state *can* become a bottleneck (how many do you have?). The only solution is either, to drop the lock and accept that summing all accounts might be inaccurate regarding current transfers (I think, real banks don’t have a global lock…) or you have to introduce multiple locks for the updaters and the sum routine has to get them all (which is hard to implement due to the deadlock risk) – Holger Apr 15 '15 at 11:21
  • I am working on a hexacore (hyperthreaded) beast, so there are 11 transaction threads and 1 summation. That might support your theory. – Kugelblitz Apr 15 '15 at 11:32
  • To add to the previous comment, the previous version seems to have a similar behaviour – Kugelblitz Apr 15 '15 at 11:53
2

You would generally use either a lock or synchronized, it is unusual to use both at once.

To manage your scenario you would normally use a fine-grained lock on each account rather than a coarse one as you have. You would also implement the totalling mechanism using a listener.

public interface Listener {

    public void changed(int oldValue, int newValue);
}

public class Account {

    private int id;
    private int balance;
    protected ReadWriteLock lock = new ReentrantReadWriteLock();
    List<Listener> accountListeners = new ArrayList<>();

    public Account(int id) {
        this.id = id;
        this.balance = 0;
    }

    public int getBalance() {
        int localBalance;
        lock.readLock().lock();
        try {
            localBalance = this.balance;
        } finally {
            lock.readLock().unlock();
        }
        return localBalance;
    }

    public void setBalance(int balance) {
        if (balance < 0) {
            throw new IllegalArgumentException("Negative balance");
        }
        // Keep track of the old balance for the listener.
        int oldValue = this.balance;
        lock.writeLock().lock();
        try {
            this.balance = balance;
        } finally {
            lock.writeLock().unlock();
        }
        if (this.balance != oldValue) {
            // Inform all listeners of any change.
            accountListeners.stream().forEach((l) -> {
                l.changed(oldValue, this.balance);
            });
        }
    }

    public boolean lock() throws InterruptedException {
        return lock.writeLock().tryLock(1, TimeUnit.SECONDS);
    }

    public void unlock() {
        lock.writeLock().unlock();
    }

    public void addListener(Listener l) {
        accountListeners.add(l);
    }

    public int getId() {
        return this.id;
    }

}

public class BankingSystem {

    protected List<Account> accounts;

    public boolean transfer(Account from, Account to, int amount) throws InterruptedException {
        if (from.getId() != to.getId()) {
            if (from.lock()) {
                try {
                    if (from.getBalance() < amount) {
                        return false;
                    }
                    if (to.lock()) {
                        try {
                            // We have write locks on both accounts.
                            from.setBalance(from.getBalance() - amount);
                            to.setBalance(to.getBalance() + amount);
                        } finally {
                            to.unlock();
                        }

                    } else {
                        // Not sure what to do - failed to lock the account.
                    }
                } finally {
                    from.unlock();
                }

            } else {
                // Not sure what to do - failed to lock the account.
            }
        }
        return true;
    }

    // Rest of class..
}

Note that you can take a write lock in the same thread twice - the second one is also allowed. Locks only exclude access from other threads.

OldCurmudgeon
  • 64,482
  • 16
  • 119
  • 213
  • Well as for the fine- vs coarse-granularity problem; After taking Holger's advice I do not have any locking anymore in the Accounts themselves, so the only locking going on would be the lock for the summation. However, after reading your answer, this might of course be a solution to get rid of that lock aswell, improving the throughput again. I will consider implementing this way of summation instead, thanks a lot – Kugelblitz Apr 15 '15 at 11:01
  • 1
    @Phil - I've added a sample `BankingSystem` for you too so you can see how you can lock two accounts for transfer. – OldCurmudgeon Apr 15 '15 at 11:03
  • 1
    Don’t lock two accounts; that’s a recipe for deadlocks. What happens if one threads tries to transfer from *a* to *b* while another tries to transfer from *b* to *a*? By the way, there is no need for holding two locks. The whole purpose of the global lock is to avoid pending transfers when summing all accounts, not to avoid concurrent transfers. – Holger Apr 15 '15 at 11:07
  • @Holger - Good call - I've changed it to a `tryLock` which should avoid the deadlock. Other techniques may be appropriate depending on intent as you say. – OldCurmudgeon Apr 15 '15 at 11:10
  • So what do you do when `tryLock` returns `false`? Simply ignoring that you don’t own the lock isn’t a proper solution. – Holger Apr 15 '15 at 11:12
  • @Holger - I'd forgotten about it's return value. I'll wrap it in some `if` statements. – OldCurmudgeon Apr 15 '15 at 11:13