1. Overview
In this tutorial, we’ll discuss the differences between Collections.synchronizedMap() and ConcurrentHashMap**.
Additionally, we’ll look at the performance outputs of the read and write operations for each.
2. The Differences
Collections.synchronizedMap() and ConcurrentHashMap both provide thread-safe operations on collections of data.
The Collections utility class provides polymorphic algorithms that operate on collections and return wrapped collections. Its synchronizedMap() method provides thread-safe functionality.
As the name implies, synchronizedMap() returns a synchronized Map backed by the Map that we provide in the parameter. To provide thread-safety, synchronizedMap() allows all accesses to the backing Map via the returned Map.
ConcurrentHashMap was introduced in JDK 1.5 as an enhancement of HashMap that supports high concurrency for retrievals as well as updates. HashMap isn’t thread-safe, so it might lead to incorrect results during thread contention.
The ConcurrentHashMap class is thread-safe. Therefore, multiple threads can operate on a single object with no complications.
In ConcurrentHashMap, read operations are non-blocking, whereas write operations take a lock on a particular segment or bucket. The default bucket or concurrency level is 16, which means 16 threads can write at any instant after taking a lock on a segment or bucket.
2.1. ConcurrentModificationException
For objects like HashMap, performing concurrent operations is not allowed. Therefore, if we try to update a HashMap while iterating over it, we will receive a ConcurrentModificationException. This will also occur when using synchronizedMap():
@Test(expected = ConcurrentModificationException.class)
public void whenRemoveAndAddOnHashMap_thenConcurrentModificationError() {
Map<Integer, String> map = new HashMap<>();
map.put(1, "baeldung");
map.put(2, "HashMap");
Map<Integer, String> synchronizedMap = Collections.synchronizedMap(map);
Iterator<Entry<Integer, String>> iterator = synchronizedMap.entrySet().iterator();
while (iterator.hasNext()) {
synchronizedMap.put(3, "Modification");
iterator.next();
}
}
However, this is not the case with ConcurrentHashMap:
Map<Integer, String> map = new ConcurrentHashMap<>();
map.put(1, "baeldung");
map.put(2, "HashMap");
Iterator<Entry<Integer, String>> iterator = map.entrySet().iterator();
while (iterator.hasNext()) {
map.put(3, "Modification");
iterator.next()
}
Assert.assertEquals(3, map.size());
2.2. null Support
Collections.synchronizedMap() and ConcurrentHashMap handle null keys and values differently.
ConcurrentHashMap doesn’t allow null in keys or values:
@Test(expected = NullPointerException.class)
public void allowNullKey_In_ConcurrentHasMap() {
Map<String, Integer> map = new ConcurrentHashMap<>();
map.put(null, 1);
}
However, when using Collections.synchronizedMap(), null support depends on the input Map*.* We can have one null as a key and any number of null values when Collections.synchronizedMap() is backed by HashMap or LinkedHashMap, whereas if we’re using TreeMap, we can have null values but not null keys.
Let’s assert that we can use a null key for Collections.synchronizedMap() backed by a HashMap:
Map<String, Integer> map = Collections
.synchronizedMap(new HashMap<String, Integer>());
map.put(null, 1);
Assert.assertTrue(map.get(null).equals(1));
Similarly, we can validate null support in values for both Collections.synchronizedMap() and ConcurrentHashMap.
3. Performance Comparison
Let’s compare the performances of ConcurrentHashMap versus Collections.synchronizedMap(). In this case, we’re using the open-source framework Java Microbenchmark Harness (JMH) to compare the performances of the methods in nanoseconds.
We ran the comparison for random read and write operations on these maps. Let’s take a quick look at our JMH benchmark code:
@Benchmark
public void randomReadAndWriteSynchronizedMap() {
Map<String, Integer> map = Collections.synchronizedMap(new HashMap<String, Integer>());
performReadAndWriteTest(map);
}
@Benchmark
public void randomReadAndWriteConcurrentHashMap() {
Map<String, Integer> map = new ConcurrentHashMap<>();
performReadAndWriteTest(map);
}
private void performReadAndWriteTest(final Map<String, Integer> map) {
for (int i = 0; i < TEST_NO_ITEMS; i++) {
Integer randNumber = (int) Math.ceil(Math.random() * TEST_NO_ITEMS);
map.get(String.valueOf(randNumber));
map.put(String.valueOf(randNumber), randNumber);
}
}
We ran our performance benchmarks using 5 iterations with 10 threads for 1,000 items. Let’s see the benchmark results:
Benchmark Mode Cnt Score Error Units
MapPerformanceComparison.randomReadAndWriteConcurrentHashMap avgt 100 3061555.822 ± 84058.268 ns/op
MapPerformanceComparison.randomReadAndWriteSynchronizedMap avgt 100 3234465.857 ± 60884.889 ns/op
MapPerformanceComparison.randomReadConcurrentHashMap avgt 100 2728614.243 ± 148477.676 ns/op
MapPerformanceComparison.randomReadSynchronizedMap avgt 100 3471147.160 ± 174361.431 ns/op
MapPerformanceComparison.randomWriteConcurrentHashMap avgt 100 3081447.009 ± 69533.465 ns/op
MapPerformanceComparison.randomWriteSynchronizedMap avgt 100 3385768.422 ± 141412.744 ns/op
The above results show that ConcurrentHashMap performs better than Collections.synchronizedMap().
4. When to Use
We should favor Collections.synchronizedMap() when data consistency is of utmost importance, and we should choose ConcurrentHashMap for performance-critical applications where there are far more write operations than there are read operations.
This is because the Collections.synchronizedMap() requires each thread to acquire a lock on the entire object for both read/write operations. By comparison, the ConcurrentHashMap allows threads to acquire locks on separate segments of the collection, and make modifications at the same time.
5. Conclusion
In this article, we’ve demonstrated the differences between ConcurrentHashMap and Collections.synchronizedMap(). We’ve also shown the performances of both of them using a simple JMH benchmark.
As always, the code samples are available over on GitHub.