Is ConcurrentHashMap always faster than HashMap for concurrent access ?
If the map is accessible by concurrent threads, ConcurrentHashMap without additional synchronization mechanisms is often enough for reading operations.
We may have some delay in the returned values by the Map but that is just delay.
If that is acceptable for you, it is fine. Otherwise, ConcurrentHashMap is probably not what you need.
But about writing operations, ConcurrentHashMap often needs additional synchronization mechanisms to ensure the data consistency.
We rarely put values in a map with map.put(key, value)
without additional computations.
It is indeed very common to check the content of the map before putting the element or to retrieve the actual value associated to a key and to enrich it rather than only overwrite it.
To ensure data consistency, these statements have to be synchronized.
So external synchronization for a ConcurrentHashMap object is very often unavoidable.
The following microbenmark compares HashMap and ConcurrentHashMap metrics both for put() and get() operations.
Here are the conclusions :
– writing operations are not less or more fast whatever the way.
– reading operations are (48X) drastically faster with ConcurrentHashMap.
Our scenario :
We have two classes (one using a HashMap, the other using a ConcurrentHashMap) that provides two methods :
– addLetterOccurence(String)
that updates the occurrence of encountered letters.
The method takes as parameter a String that may contain zero or one alphabetical character.
When a alphabetical character is found, we update the occurrence of the current letter in a Map instance field.
– getOccurence(Character)
that returns the occurrence of the Character passed as parameter.
Here is the OccurrenceCounterWithHashMap class :
import java.util.HashMap; import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern; public class OccurrenceCounterWithHashMap { private Map<Character, Integer> mapCountByLetter = new HashMap<>(); public void addLetterOccurence(String word) { Character letter = retrieveCharacter(word); if (letter == null) { return; } synchronized (this) { Integer occurrence = mapCountByLetter.get(letter); if (occurrence == null) { occurrence = 0; } mapCountByLetter.put(letter, ++occurrence); } } public int getOccurence(Character letter) { Integer occurrence = null; synchronized (this) { occurrence = mapCountByLetter.get(letter); } if (occurrence == null) { occurrence = 0; } return occurrence; } private Character retrieveCharacter(String word) { Matcher matcher = Pattern.compile("([A-Za-z])") .matcher(word); if (matcher.find()) { String group = matcher.group(1); return group.charAt(0); } return null; } } |
Note that HashMap.get()
is invoked in a synchronized statement. Why ?
Because HashMap methods are not designed to be accessed concurrently. More specifically, the HashMap javadoc states :
Note that this implementation is not synchronized. If multiple threads access a hash map concurrently, and at least one of the threads modifies the map structurally, it must be synchronized externally.
It means that for example invoking HashMap.get()
in a foo() method by a thread A while HashMap.put()
is invoked in a bar() method by a thread B may create a undesirable and unpredictable behavior if no explicit synchronization is performed.
To get more information about synchronization with HashMap, you can read this article.
Here is the OccurrenceCounterWithHashMap class :
import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.regex.Matcher; import java.util.regex.Pattern; public class OccurrenceCounterWithConcurrentHashMap { private Map<Character, Integer> mapCountByLetter = new ConcurrentHashMap<>(); public void addLetterOccurence(String word) { Character letter = retrieveCharacter(word); if (letter == null) { return; } synchronized (this) { Integer occurrence = mapCountByLetter.get(letter); if (occurrence == null) { occurrence = 0; } mapCountByLetter.put(letter, ++occurrence); } } public void addLetterOccurenceWithComputeMethod(String word) { Character letter = retrieveCharacter(word); if (letter == null) { return; } mapCountByLetter.compute(letter, (c, i) -> i == null ? 1 : ++i); } public int getOccurence(Character letter) { Integer occurrence = mapCountByLetter.get(letter); if (occurrence == null) { occurrence = 0; } return occurrence; } private Character retrieveCharacter(String word) { Matcher matcher = Pattern.compile("([A-Za-z])") .matcher(word); if (matcher.find()) { String group = matcher.group(1); return group.charAt(0); } return null; } } |
Note that we have an additional addLetter
method for the ConcurrentHashMap case : addLetterOccurenceWithComputeMethod()
.
Functionally, it performs the same thing as addLetterOccurence()
. It only does that in a different way : it uses the Map.compute()
default method added in Java 8.
The JMH benchmark class :
import java.util.concurrent.TimeUnit; import java.util.stream.IntStream; import org.openjdk.jmh.annotations.Benchmark; import org.openjdk.jmh.annotations.BenchmarkMode; import org.openjdk.jmh.annotations.Level; import org.openjdk.jmh.annotations.Mode; import org.openjdk.jmh.annotations.OutputTimeUnit; import org.openjdk.jmh.annotations.Scope; import org.openjdk.jmh.annotations.Setup; import org.openjdk.jmh.annotations.State; import org.openjdk.jmh.annotations.Threads; import org.openjdk.jmh.runner.Runner; import org.openjdk.jmh.runner.RunnerException; import org.openjdk.jmh.runner.options.Options; import org.openjdk.jmh.runner.options.OptionsBuilder; @State(Scope.Thread) @BenchmarkMode(Mode.AverageTime) @OutputTimeUnit(TimeUnit.NANOSECONDS) @Threads(10) public class MapBenchmark { private int charNumericValue; private OccurrenceCounterWithHashMap occurrenceCounterWithHashMap = new OccurrenceCounterWithHashMap(); private OccurrenceCounterWithConcurrentHashMap occurrenceCounterConcurrentHashMap = new OccurrenceCounterWithConcurrentHashMap(); public static void main(String[] args) throws RunnerException { Options opt = new OptionsBuilder().include(MapBenchmark.class.getSimpleName()) .warmupIterations(5) .measurementIterations(5) .forks(1) .build(); new Runner(opt).run(); } @State(Scope.Benchmark) public static class HashMapStatementsState { public OccurrenceCounterWithHashMap occurrenceCounterSynchStatements = new OccurrenceCounterWithHashMap(); @Setup(Level.Iteration) public void doSetup() { IntStream.range(0, 50) .forEach(i -> { final String string = String.valueOf((char) i); occurrenceCounterSynchStatements.addLetterOccurence(string); }); } } @State(Scope.Benchmark) public static class ConcurrentHashMapState { public OccurrenceCounterWithConcurrentHashMap occurrenceCounterConcurrentHashMap = new OccurrenceCounterWithConcurrentHashMap(); @Setup(Level.Iteration) public void doSetup() { IntStream.range(0, 50) .forEach(i -> { final String string = String.valueOf((char) i); occurrenceCounterConcurrentHashMap.addLetterOccurence(string); }); } } @Benchmark public void _1_synchStatementsAddLetterOccurence() { occurrenceCounterWithHashMap.addLetterOccurence("123456" + getNextLetter_50_distinct()); } @Benchmark public void _2_concurrentHashMapAddLetterOccurence() { occurrenceCounterConcurrentHashMap.addLetterOccurence("123456" + getNextLetter_50_distinct()); } @Benchmark public void _3_concurrentHashMapAddLetterOccurenceWithComputeMethod() { occurrenceCounterConcurrentHashMap.addLetterOccurenceWithComputeMethod("123456" + getNextLetter_50_distinct()); } @Benchmark public void _4_synchStatementsGetOccurence(HashMapStatementsState synchronizedStatementsState) { synchronizedStatementsState.occurrenceCounterSynchStatements.getOccurence(getNextLetter_10_distinct()); } @Benchmark public void _5_concurrentHashMapGetOccurence(ConcurrentHashMapState concurrentHashMapState) { concurrentHashMapState.occurrenceCounterConcurrentHashMap.getOccurence(getNextLetter_10_distinct()); } private synchronized char getNextLetter_50_distinct() { if (charNumericValue >= 49) { charNumericValue = 0; } return (char) ++charNumericValue; } private synchronized char getNextLetter_10_distinct() { if (charNumericValue >= 9) { charNumericValue = 0; } return (char) ++charNumericValue; } } |
The benchmark results (less is faster):
# Run complete. Total time: 00:00:52 Benchmark Mode Cnt Score Error Units MapBenchmark._1_synchStatementsAddLetterOccurence avgt 5 2398,325 ± 9,521 ns/op MapBenchmark._2_concurrentHashMapAddLetterOccurence avgt 5 2401,879 ± 10,965 ns/op MapBenchmark._3_concurrentHashMapAddLetterOccurenceWithCompute avgt 5 2391,523 ± 7,598 ns/op MapBenchmark._4_synchStatementsGetOccurence avgt 5 1974,261 ± 20,800 ns/op MapBenchmark._5_concurrentHashMapGetOccurence avgt 5 41,796 ± 1,194 ns/op |