Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to properly lock a resource while created in Java

I have this singleton that creates objects, currently it looks like this:

public ApplicationManagerSingleton {
    ....
    private Map<String, Thing> map = new HashMap<String, Thing>();

    public Thing getThingById( String id ) {
       Thing t = null;
       if ( !map.contains(id) ) {
        t = longAndCostlyInitializationOfThing();
        map.put(id, t );
       }
       return map.get(id);
     }
 }

The obvious problem it has is, if two threads try to access the same thing, they may endup duplicating the thing.

So I used a lock:

 public ApplicationManagerSingleton {
      private Map<String, Thing> map = new HashMap<Sring, Thing>();
      public Thing getThingById(String id ) {
          synchronized( map ) {
             if (!map.contains(id)) {
                 t = initialize....
             }
             map.put(id, t);
           }
           returns map.get(id);
      }
 }

But now that's worst because I'll be locking the map for a while each time a new resource is being created in demerit of the other threads wanting different things.

I'm pretty sure it can be better with Java 5 concurrent package. Can somebody point me in the right direction?

What I want to avoid is to lock the class or the map for other threads that are interested in other things.

like image 880
OscarRyz Avatar asked Dec 26 '22 11:12

OscarRyz


2 Answers

Maybe ConcurrentHashMap can help you. As its name implies, it supports concurrent modifications.

To create a new element only once, you could do something like this:

private Map<String,Thing> map = new ConcurrentHashMap<>();
private final Object lock = new Object();
public Thing getById(String id) {
  Thing t = map.get(id);
  if (t == null) {
    synchronized(lock) {
      if (!map.containsKey(id)) {
        t = //create t
        map.put(id, t);
      }
    }
  }
  return t;
}

Only one thread is allowed to create new stuff at a time, but for existing values there is no locking whatsoever.

If you want to avoid locks completely you'd have to use 2 maps but it gets somewhat convoluted and it's only worth it if you really expect many threads to be populating the map continually. For that case it might be better to use FutureTasks together with a thread pool to create the objects asynchronously, minimizing the time the lock is in place (you still need a lock so that only one thread creates the new element).

The code would be something like this:

private Map<String,Future<Thing>> map = new ConcurrentHashMap<>();
private final Object lock = new Object();
ExecutorService threadPool = ...;
public Thing getById(String id) {
  Future<Thing> t = map.get(id);
  if (t == null) {
    synchronized(lock) {
      if (!map.containsKey(id)) {
        Callable<Thing> c = //create a Callable that creates the Thing
        t = threadPool.submit(c);
        map.put(id, t);
      }
    }
  }
  return t.get();
}

The lock will only be in place for the time it takes to create the Callable, submit it to the thread pool to get a Future, and put that Future in the map. The Callable will create the element in the thread pool and when it returns the element, the get() method of the Future will unlock and return its value (for any threads that are waiting; subsequent calls won't lock).

like image 124
Chochos Avatar answered Jan 08 '23 01:01

Chochos


If you want to prevent creating the item multiple times which also blocking as little as possible, I would use two maps. One for holding a set of locks for use when creating the object and one for holding the objects.

Consider something like this:

private ConcurrentMap<String, Object> locks = new ConcurrentHashMap<String, Object>();
private ConcurrentMap<String, Thing> things = new ConcurrentHashMap<String, Thing>();

public void Thing getThingById(String id){
    if(!things.containsKey(id)){        
      locks.putIfAbsent(id, new Object());
      synchronized(locks.get(id)){
         if (!things.containsKey(id)){
             things.put(id, createThing());
         }
      }
    }

    return things.get(id);
}

This will only block multiple thread attempting to get the same key, while preventing the Thing being created twice for the same key.

Update

Guava Cache example moved to new Answer.

like image 22
John B Avatar answered Jan 08 '23 01:01

John B