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.
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).
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With