简体   繁体   中英

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.

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.

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).

After looking into it, I thing Guava's LoadingCache is probably a very nice solution for this. By default the CacheBuilder will create a cache that does not do any eviction (so it is just a map) and has the thread-blocking when loading for a key already built in.

LoadingCache

CacheBuilder

 private Cache<String, Thing> myCache;

 MyConstructor(){
    myCache = CacheBuilder.newBuilder().build(
       new CacheLoader<String, Thing>() {
         public Thing load(String key) throws AnyException {
           return createExpensiveGraph(key);
         }
        });
 }

  public void Thing getThingById(String id){
    return myCache.get(id);
  }

You could immediatley insert a lightweight proxy in the map. The proxy will delegate to the real object once it is initialized, but block until then. Once the real Thing is initialized, it can replace the proxy in the map.

private Map<String, Thing> map = new ConcurrentHashMap<>();

public Thing getThingById(String id) {
    ThingProxy proxy = null;
    synchronized (map) {
        if (!map.containsKey(id)) {
            proxy = new ThingProxy();
            map.put(id, proxy);
        }
    }
    if (proxy != null) {
        proxy.initialize();
        map.put(id, proxy.getRealThing());
    }
    return map.get(id);
}

private class ThingProxy implements Thing {

    private Thing realThing;
    private CountDownLatch countDownLatch = new CountDownLatch(1);

    @Override
    public void someMethodOfThing() {
        try {
            countDownLatch.await();
            realThing.someMethodOfThing();
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    }

    public void initialize() {
        realThing = longAndCostlyInitializationOfThing();
        countDownLatch.countDown();
    }

    public Thing getRealThing() {
        return realThing;
    }
}

This locks on the map, but only briefly to create a proxy and put it if needed.

The code for the proxy may become laborious, in which case you may be better of creating a proxy using reflection (see java.lang.reflect.Proxy )

You could use a ConcurrentHashMap . This one can be accessed by multiple threads without problem.

I tried these solutions and they failed at some point in my implementation, not saying they wont work in other scenarios.

What I finally end up doing was to use a ConcurrentMap to check if the resource has been already requested or not. If not, it is created and stored somewhere else.

... 
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.ConcurrentHashMap;

... 
private ConcurrentMap<String, Boolean> created = new ConcurrentMap<>();
....
if ( created.putIfAbsent( id, Boolean.TRUE ) == null ) {
    somewhereElse.put( id, createThing() );
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM