0

How do I acquire lock on multiple items?

Consider the example below,

Map<String, Account> accountMap = new HashMap<>(); // key=accountNumber,value=AccountObject

class Account{
     private String accountNumber; // getter & setter
     private double accountBalance; // getter & setter
}

I need to transfer funds from one account to another, so I was thinking of having a nested synchronized block and realized that it would lead to deadlock.

// bad code
synchronized(accountMap.get(accountNumber1)){
    synchronized(accountMap.get(accountNumber2)){
     // business logic
    }
}

Also, I don't want a single lock because it would block the processing of all the threads for one transaction. Something like below

//bad code
Object mutex = new Object();

synchronized(mutex){
     // business logic with accountNumber1 & accountNumber2
}

How do I go about solving this issue? I need to maintain locks only for two account objects.

Also, possible duplicate(but I wanted to know if there are different solutions). Preventing deadlock in a mock banking database

  • Use timed lock acquisition, like [Lock.tryLock(long time, TimeUnit unit)](https://docs.oracle.com/javase/7/docs/api/java/util/concurrent/locks/Lock.html#tryLock(long,%20java.util.concurrent.TimeUnit)) – Jason Law Jan 10 '20 at 11:28

2 Answers2

2

It is not possible to obtain a single lock on multiple objects. But there are alternatives.

  1. You can use a shared (aka "global") lock. The problem is that this lock could be a bottleneck.

  2. You can use some other object as a proxy for the two objects, and obtain a lock on that. For example, assuming that your accounts have unique ids:

    String lockName = (acc1.getAccountNumber() + "-" 
                       + acc2.getAccountNumber()).intern();
    synchronized (lockName) {
        // we now have a lock on the combination of acc1 and acc2.
    }
    

    (However, this probably doesn't work in your use-case, because it doesn't stop a simultaneous transfer involving one of the two accounts and a third one.)

  3. Obtain the locks in a canonical order. For example.

    if (acc1.getAccountNumber().compareTo(acc2.getAccountNumber()) < 0) {
        synchronized (acc1) {
            synchronized (acc2) {
                // do transfer
        }
    } else {
        synchronized (acc2) {
            synchronized (acc1) {
                // do transfer
            }
        }
    }
    

    If locks are obtained in the same order by all threads, deadlock is not possible.

  4. Acquire the locks using Lock.tryLock with a times. However this has a couple of problems:

    • You now need to manage per-account Lock objects.
    • There is the (theoretical) problem of "livelock". To address this you can use randomly generated timeout values.

(Note: don't attempt to use the identity hashcode as a proxy for a unique id. Identity hashcodes are not unique, and you also have the problem that you could have multiple in-memory Account objects that represent the same logical account ... if they are DTOs.)

Stephen C
  • 698,415
  • 94
  • 811
  • 1,216
0

You can lock on two objects and still avoid the deadlock using lock ordering

if (accountNumber1 < accountNumber2) {
   synchronized (accountMap.get(accountNumber1)) {
       synchronized (accountMap.get(accountNumber2)) {
           //
       }
   }
} else {
       synchronized (accountMap.get(accountNumber2)) {
           synchronized (accountMap.get(accountNumber1)) {
               //
           }
       }
   }
}

This way we avoid the circular wait.

I assume that your Hashmap writes are thread-safe or it is read-only.

Sleiman Jneidi
  • 22,907
  • 14
  • 56
  • 77