1

I am implementing a parallel banking system, where all operations can run concurrently. I have implemented a thread safe transferMoney method, that transfers amount from Account from to to.

transferMoney is implemented with the following code:

public boolean transferMoney(Account from, Account to, int amount) {
        if (from.getId() == to.getId()){
            return false;
        }else if(from.getId() < to.getId()) {
            synchronized(to) {
                synchronized(from) {
                    if(from.getBalance() >= amount) {
                        from.setBalance(from.getBalance()-amount);
                        to.setBalance(to.getBalance()+amount);
                    }else {
                        return false;
                    }
                }
            }
        }else {
            synchronized(from) {
                synchronized(to) {
                    if(from.getBalance() >= amount) {
                        from.setBalance(from.getBalance()-amount);
                        to.setBalance(to.getBalance()+amount);
                    }else {
                        return false;
                    }
                }
            }
        }

        return true;
    }

To prevent deadlocks, I have specified that the locks are always acquired in the same order. To assure that the locks are acquired in the same order, I am using the unique ID of Account.

Additionally, I have implemented a method that sums up the total amount of money in the bank with the following code:

public int sumAccounts(List<Account> accounts) {
    AtomicInteger sum = new AtomicInteger();

    synchronized(Account.class) {
        for (Account a : accounts) {
            sum.getAndAdd(a.getBalance());
        }
    }

    return sum.intValue();
}

Problem

When I run sumAccounts() concurrently with transferMoney(), I will end up with more (sometimes less) money in the bank before, even though no money was added. From my understanding if I lock all Account objects via synchronized(Account.class), shouldn't I get the correct sum of the bank as I am blocking the execution of transferMoney()?

What I have tried this far

I have tried the following things:

  • synchronizing Account.class like above (doesn't work)
  • synchronizing the particular account in the for each loop (but of course this isn't thread safe as transactions are happening concurrently)
  • synchronizing both methods via a ReentrantLock object. This works, but it takes a huge hit on performance (takes three times as much as the sequential code)
  • synchronizing both methods on class level. This also works, but again takes three times longer than running the operations sequentially.

Shouldn't the lock on Account.class prevent any further transferMoney() executions? If not, how can I fix this issue?

Edit: The code for getBalance():

public int getBalance() {
        return balance;
}
pr0f3ss
  • 527
  • 1
  • 4
  • 17
  • 2
    Synchronizing on Account.class does not acquire the monitors on any Account instance; see also https://stackoverflow.com/questions/2056243/java-synchronized-block-for-class – Daniele Apr 06 '19 at 15:02
  • Is `Account.getBalance()` synchronized method? If not, you will be getting the balance just before if is subtracted by an account and adding the balance of the newly added account – Cratylus Apr 06 '19 at 16:57
  • @Cratylus No it is not synchronized as I am locking the objects when accessing them in `transferMoney()`. I added it though to test and it still gives me the wrong result. – pr0f3ss Apr 06 '19 at 19:24
  • @pr0f3ss: Can you post the code for `getBalance()`? – Cratylus Apr 07 '19 at 10:56
  • @Cratylus just added it as an edit to the question – pr0f3ss Apr 07 '19 at 14:19

3 Answers3

1

You can use ReadWriteLock for this case. transferMoney method will use the read lock, so it can be executed concurrently. sumAccounts method will use the write lock, so when it is executing no transferMoney(or sumAccounts) can be executed from other threads.

Using ReentrantLock and synchronizing both methods on class level, will behave the same as You have stated because they will not let concurrent execution of transferMoney method.

sample code:

final ReadWriteLock rwl = new ReentrantReadWriteLock();

public boolean transferMoney(Account from, Account to, int amount) {
  rwl.readLock().lock();
  try{
    .... Your current code here
  }
  finally {
       rwl.readLock().unlock();
  }
}

public int sumAccounts(List<Account> accounts) {
  rwl.writeLock().lock();
  try{
    // You dont need atomic integer here, because this can be executed by one thread at a time
    int sum = 0;
    for (Account a : accounts) {
        sum += a.getBalance();
    }
    return sum;
  }
  finally {
       rwl.writeLock().unlock();
  }
}

Also fair mode of Reentrant locks will tend to perform slower than non-fair modes. Check the docs for details.

https://docs.oracle.com/javase/7/docs/api/java/util/concurrent/locks/ReentrantReadWriteLock.html

miskender
  • 7,460
  • 1
  • 19
  • 23
  • Just ran this code. It actually returns an incorrect sum (don't know why though as previously I had implemented it with just a normal `ReentrantLock` object and it worked). – pr0f3ss Apr 06 '19 at 16:56
  • @pr0f3ss If this gives wrong result, it might be because of, actions done by threads in transferMoney is not visible to thread that execute sumAccounts method. I thought lock will guarantee this happens before relationship. You can try making balance field volatile. (I am assuming there is no bug in other parts of Your code. – miskender Apr 06 '19 at 17:09
  • `sumAccounts` indeed is executed in a new `Thread` object when ran. I got it to work eventually, but the performance is still very bad like all things that worked so far. – pr0f3ss Apr 06 '19 at 19:29
  • @pr0f3ss What did You do to make it work. This should perform better than Reentrantlock. – miskender Apr 06 '19 at 20:10
  • 1
    I used `rwl.readLock.lock()` on both previously (Dumb I know). The trick was to create a lock inside the `Account` class and lock and unlock from there. So in the first for each loop, you are locking the account, then adding its balance to the sum and finally in the following for each loop you unlock all the accounts in the same order. This guarantees that no transactions are happening concurrently. Do you think it is appropriate to answer my own question to answer the issue? – pr0f3ss Apr 07 '19 at 14:49
  • @pr0f3ss You can answer Your questions. But rwl already guarantees when sumAccounts executing, no transferMoney method can be executed. ( Just to be clear; .... Your current code here section in this answer refers to the exact code of Your transferMoney method in the question.) – miskender Apr 07 '19 at 18:53
1

As stated in a comment, taking a lock on a class object won't take locks on all instances of that class, it will just take a lock on the Class object representing your Account class. That lock is not incompatible with locks on Account objects, so you have no synchronizing going on at all.

Taking locks on individual Account objects could be done inside your for loop (in sumAccounts) but it won't prevent schedules like this happening :

- sumAccounts locks 'first' Account and reads balance (and releases lock again at end of the synchronized block taking the lock)
- system schedules a moneyTransfer() from 'first' to 'last'
- sumAccounts locks 'last' Account and reads balance, which includes the amount that was just transferred from 'first' and was already included in the sum

So if you want to prevent that too you need to synchronize the moneyTransfer() processing on Account.class too (which then obsoletes the need for locking on the indivudual objects).

Erwin Smout
  • 18,113
  • 4
  • 33
  • 52
  • You are correct. But by synchronizing both methods via the same lock, I get worse performance than executing it sequentially. (See question - What I have tried this far - Bullet points 3 and 4) – pr0f3ss Apr 06 '19 at 19:34
  • ***OF COURSE*** you get "worse performance" (convoy syndrome, in particular). That's just the price to pay for better integrity (and better guarantee of result correctness). The main trouble with java locks as held by synchronized() blocks is that they are always exclusive locks exclusively (java did not have any concept such as "shared" locks that the DBMS's do have - which is what permits them to achieve higher parallellism while maintaining better integrity guarantees). Too much to say on the matter compared to what fits in the space here. – Erwin Smout Apr 06 '19 at 20:37
  • See that other reply on ReadWriteLock. (And observe that if you do all this stuff using database technology, you'd be getting all of this for free without having to write one single line of code and often without having to spend one second of thinking about "what type of lock" or such issues. – Erwin Smout Apr 06 '19 at 20:40
  • Yes I know that I will get worse performance, but even when I run `transferMoney()` concurrently separately (without `sumAccounts()`) I get like three to four times worse performance. – pr0f3ss Apr 07 '19 at 14:21
0

It is very hard to review your code because we have no way of knowing if the object accounts that you synchronize on, are the exact same instances in all the functions.
First of all, we have to agree if the sum of balances and the transfer of amounts are two operations that should be running at the same time.
I would expect that the sum of balances is the same before and after the transfer of amounts.
Additionally you are using synchronized(Account.class) in the sum of balances which is wrong. You should be synchronizing on the objects you are looping over.
Now even if you indeed are coordinating in the exact same instances you can still have the following schedule:

Thread-1 (transfer)  
  locks from  
Thread-2 (sum balance)  
  locks first object in the list and adds the balance to the running sum and moves to next object
Thread-1  
   locks to (which is the object Thread-2) processed
   moves money from => to  

You already summed the to with the amount before the increase and you could be adding from with the amount after the deduction depending on the scheduling.

The problem is that you are updating 2 objects in the transfer but only locking 1 in the sum.
What I would suggest is either:

  1. either synchronize both methods on the same lock and make them run serially
  2. set some dirty flag when the objects go in the transfer method and if that is set, skip them in the sum of balance and finish the sum when all the updates are done
  3. Why are you even doing this in Java? This should be happening in the database using transactions with ACID properties.
Cratylus
  • 52,998
  • 69
  • 209
  • 339