ConcurrentHashMap
has been pitched as a simple alternative for HashMap
, eliminating the need for a synchronized blocks. I had some simple event counting code that created count records on the fly. Although I could have used synchronized blocks for safety I used ConcurrentHashMap
for this situation, partly for efficiency but mostly for the exercise. Going through this made me realize how carefully ConcurrentHashMap
must be used for your code to work correctly and efficiently.
When using a HashMap
, the standard idiom to add a value if it doesn’t exist is to use code that looks something like this:
synchronized (this) { Record rec = records.get(id); if (rec == null) { rec = new Record(id); records.put(id, rec); } return rec; }
If you were to simply replace HashMap
with ConcurrentHashMap
and remove the synchronized
keyword your code would be exposed to a race condition. If a new Record
was put into the map just after the call to get
returned null the put
operation would overwrite the value. You could addsynchronized
back in but this defeats the purpose of using ConcurrentHashMap
.
To safely create values on demand you must use putIfAbsent
(and avoid making extra calls to get
in the process).
First check to see if a value with the key already exists in the map and use this value if it does. Otherwise, create a new value for the map and add it with putIfAbsent
. putIfAbsent
returns any existing value if there is one, otherwise null (this is why ConcurrentHashMap
can’t contain null values).
private ConcurrentMap<String, Record> records = new ConcurrentHashMap<String, Record>(); private Record getOrCreate(String id) { Record rec = records.get(id); if (rec == null) { // record does not yet exist Record newRec = new Record(id); rec = records.putIfAbsent(id, newRec); if (rec == null) { // put succeeded, use new value rec = newRec; } } return rec; }
If putIfAbsent
does return a value, it’s the one that must be used. It may have already been used by other threads at this point. The new value created must be abandoned. Although it sounds wasteful this case should happen very infrequently.
I’ve seen other code on the net that ignores the return value ofputIfAbsent
and makes another call to get
at the end to figure out which value made it into the map (the new value created or a value from another thread). Although this will work it introduces an unnecessary lookup.