I want to avoid locking on read if possible. But this “feels” like double-checked locking, even though no partially initialized members are involved.
Is this a good construct?
private final Map<String, Stuff> stash = new HashMap<String, Stuff>();
public Stuff getStuff(String name) {
if (stash.containsKey(name))
return stash.get(name);
synchronized(stash) {
if (stash.containsKey(name)) {
return stash.get(name);
}
else {
Stuff stuff = StuffFactory.create(name);
stash.put(name, stuff);
return stuff;
}
}
}
No, this construct is not thread-safe.
Assume that thread
writeris putting something into the map, and the map, being too small, must be resized. This is done inside thesynchronizedblock, so you may think you are fine.During the resizing, nothing in the map is guaranteed.
Now, at the very same time, assume that a thread
readercallsgetStufffor an existing element. This thread may access the map directly, since it doesn’t hit thesynchronizedblock for the first call tocontainsKeyandget. It will find the map at an undefined state, and although it only reads, it accesses data whose contents is undefined. Among probable results are:getStuffreturnsnullwhen it shouldn’t.getStuffreturns the intendedStuff.getStuffreturns some internal object that is used by theHashMapimplementation during resizing.getStuffreturns some otherStuff, unrelated to the name.getStuffgets caught in an infinite loop.This is just the obvious case that should be easy to understand. So no, don’t take shortcuts when there are well-designed classes like
ConcurrentHashMapor Guava’sMapMaker.By the way: Calling
containsKeyfirst and thengetwith the same key is rather inefficient. Just callget, save the result and compare it tonull. You will save one searching operation in the map.