Java HashMap vs ConcurrentHashMap: Choosing the Right Map for Concurrency
java hashmap vs concurrenthashmap: Compare Java HashMap and ConcurrentHashMap: thread safety, performance, null handling, and atomic operations to choose the right map...
java hashmap vs concurrenthashmap requires a clear understanding of the core syntax, runtime behavior, and practical implementation patterns demonstrated in the examples below.
When you need a key-value store in Java, the choice often comes down to HashMap vs ConcurrentHashMap. The decision affects thread safety, performance, and correctness under concurrent access. This article compares the two and explains when each is the right fit.
The Core Difference: Thread Safety
HashMap is not thread-safe. If multiple threads modify the same HashMap instance without external synchronization, the internal state can become corrupted. In practice, this can lead to lost updates, infinite loops during resizing (in older Java versions), or ConcurrentModificationException during iteration. Even reads can produce inconsistent results if another thread is writing concurrently.
ConcurrentHashMap is designed for concurrent access. It allows multiple threads to read and write without external synchronization, while maintaining the integrity of the map. The implementation uses internal mechanisms to ensure that operations are safe even when multiple threads are modifying different parts of the map simultaneously.
The most immediate consequence is that you cannot simply swap HashMap for ConcurrentHashMap without considering the behavioral differences. The two classes have different contracts around null keys, iteration semantics, and atomic operations.
How ConcurrentHashMap Achieves Thread Safety
ConcurrentHashMap does not lock the entire map for every operation. Instead, it uses a more granular strategy that has evolved across Java versions. In modern Java (8 and later), the implementation uses CAS (compare-and-swap) for certain operations and synchronized blocks on individual bins (or buckets) for updates that require atomicity. This allows concurrent writes to different bins to proceed in parallel, while writes to the same bin are serialized.
The exact internal layout has changed over time. Java 7 used a segment-based design where the map was divided into segments, each with its own lock. Java 8 replaced that with a bin-based approach using synchronized on the first node of a bin. The key point is that ConcurrentHashMap avoids a single global lock, which is why it scales better under high concurrency.
For reads, ConcurrentHashMap typically does not require locking at all. The get method is lock-free in most cases, relying on volatile reads and careful memory ordering. This makes read-heavy workloads particularly efficient.
Performance Characteristics
HashMap has no synchronization overhead. In a single-threaded environment, it is faster than ConcurrentHashMap for most operations because there is no atomicity machinery or memory fencing. If your map is confined to a single thread, or if you are willing to synchronize externally, HashMap is the better choice.
ConcurrentHashMap trades some raw speed for concurrency safety. The overhead comes from CAS operations, volatile reads, and occasional synchronized blocks. However, under contention, ConcurrentHashMap can outperform a HashMap wrapped with Collections.synchronizedMap because the latter locks the entire map for every operation, creating a serialization bottleneck.
Memory usage also differs. ConcurrentHashMap uses more memory per entry because it stores additional fields for concurrency control (e.g., volatile flags, node pointers). For very large maps, this overhead can be significant, but for most applications it is acceptable.
When to Use HashMap
Use HashMap when:
- The map is accessed by only one thread at a time.
- The map is created and populated before being published to multiple threads, and then never modified again (effectively immutable).
- You are willing to handle synchronization externally, for example by wrapping it with
Collections.synchronizedMapor using your own locking.
A common pattern is to build a HashMap during initialization and then expose it as an unmodifiable view. As long as no thread modifies it after publication, it is safe to read from multiple threads without synchronization.
Map<String, Integer> config = new HashMap<>(); config.put("timeout", 5000); config.put("retries", 3); Map<String, Integer> immutableConfig = Collections.unmodifiableMap(config); // Safe to share immutableConfig across threads
If you need a mutable map that is shared, HashMap alone is not enough. You could use Collections.synchronizedMap, but that introduces coarse-grained locking, which may become a performance problem under contention.
When to Use ConcurrentHashMap
Use ConcurrentHashMap when the map is shared among multiple threads and at least one thread may modify it. It is the standard choice for caches, registries, and any state that is frequently updated concurrently.
ConcurrentHashMap also provides atomic compound operations that are essential for correct concurrent updates. Methods like putIfAbsent, compute, computeIfAbsent, and merge are performed atomically. With a HashMap wrapped in synchronizedMap, these operations are not atomic unless you wrap the entire sequence in a synchronized block, which defeats the purpose of the wrapper.
ConcurrentHashMap<String, Integer> counts = new ConcurrentHashMap<>(); // Atomic increment without external locking counts.compute("visits", (key, value) -> value == null ? 1 : value + 1);
If you tried the same with a synchronizedMap, you would need to synchronize on the map to avoid a race condition:
Map<String, Integer> counts = Collections.synchronizedMap(new HashMap<>()); // This is not atomic without explicit synchronization synchronized (counts) { Integer current = counts.get("visits"); counts.put("visits", current == null ? 1 : current + 1); }
The explicit synchronization adds complexity and reduces concurrency because the entire map is locked for the duration of the operation.
Null Keys and Values
HashMap allows one null key and any number of null values. ConcurrentHashMap does not allow null keys or values. Attempting to insert a null into a ConcurrentHashMap throws NullPointerException.
This difference is not arbitrary. The designers of ConcurrentHashMap chose to disallow null because the ambiguity of a null value can be problematic in concurrent code. For example, get returns null both when a key is absent and when the value is null. In a concurrent map, distinguishing between the two would require additional synchronization. By forbidding null, ConcurrentHashMap ensures that null always means "key not present", which simplifies atomic operations.
If your data may contain null values, you cannot use ConcurrentHashMap directly. You would need to use a sentinel value or a wrapper class, or fall back to a synchronized map.
Iteration and Weak Consistency
Iterators returned by HashMap are fail-fast: if the map is structurally modified after the iterator is created, the iterator throws ConcurrentModificationException. This is a fail-fast behavior that helps catch bugs, but it is not guaranteed and can be unreliable in concurrent settings.
ConcurrentHashMap iterators are weakly consistent. They reflect the state of the map at some point since the iterator was created, but they do not throw ConcurrentModificationException. They may or may not reflect subsequent modifications, and they are guaranteed not to traverse more than once. This makes them safe to use while other threads are modifying the map, but you should not rely on seeing the latest state during iteration.
For example, iterating over a ConcurrentHashMap while another thread adds entries may or may not include the new entries. This is acceptable for many use cases, such as snapshotting or logging, but if you need a consistent snapshot, you should use snapshot methods or copy the map.
Comparing with Collections.synchronizedMap
Collections.synchronizedMap provides a simple way to make a HashMap thread-safe by wrapping it with a global lock. Every method is synchronized on the map object, so only one thread can execute any operation at a time. This is easy to use but does not scale well under contention.
ConcurrentHashMap offers better concurrency because it allows reads to proceed without locking and writes to different bins to proceed in parallel. The table below summarizes the key differences:
| Feature | HashMap | ConcurrentHashMap | Collections.synchronizedMap |
|---|---|---|---|
| Thread safety | Not safe | Safe | Safe |
| Lock granularity | None | Per-bin (or segment) | Single global lock |
| Null keys/values | Allowed | Not allowed | Allowed (depends on underlying map) |
| Atomic compound operations | Not available | Available | Not available without external sync |
| Iterator behavior | Fail-fast | Weakly consistent | Fail-fast (if underlying map is HashMap) |
| Performance under contention | N/A (not safe) | High | Low |
In practice, ConcurrentHashMap is almost always the better choice over synchronizedMap when you need thread safety and concurrency. The only reason to use synchronizedMap is if you need to support null keys/values and cannot redesign around that, or if you are working with a map type that does not have a concurrent counterpart.
Resizing and Rehashing
HashMap resizes when the number of entries exceeds the load factor threshold. During resizing, the internal array is replaced, and all entries are rehashed. If multiple threads trigger a resize concurrently, the map can be left in an inconsistent state. This is one of the most dangerous failure modes of HashMap in concurrent code.
ConcurrentHashMap also resizes, but it does so safely. The resizing process is designed to allow concurrent reads and even some writes during the resize. The implementation uses a ForwardingNode to indicate that a bin is being transferred, and threads can help with the transfer if they encounter it. This is a complex mechanism, but it ensures that no thread ever sees a partially resized map.
Because of this, ConcurrentHashMap can handle a high rate of inserts without the risk of corruption. However, resizing still requires a global rehash, which can cause a temporary performance dip. For very large maps, you can pre-size the ConcurrentHashMap to reduce the number of resizes.
Atomic Operations and Compute Patterns
The real advantage of ConcurrentHashMap over a synchronized HashMap is the set of atomic methods. These methods are essential for implementing concurrent algorithms without race conditions.
For example, putIfAbsent is often used to initialize a value only if the key is not already present:
ConcurrentHashMap<String, Connection> connections = new ConcurrentHashMap<>(); Connection conn = connections.putIfAbsent(key, createConnection()); // If conn is not null, another thread already created it
Similarly, computeIfAbsent is useful for lazy initialization:
Map<String, List<String>> index = new ConcurrentHashMap<>(); List<String> list = index.computeIfAbsent(word, w -> new CopyOnWriteArrayList<>()); list.add(articleId);
These operations are atomic with respect to the map. You cannot achieve the same effect with a synchronizedMap without wrapping the entire check-and-act sequence in a synchronized block, which is both error-prone and less efficient.
Choosing Based on Your Concurrency Requirements
To decide between HashMap and ConcurrentHashMap, ask yourself:
- Is the map ever accessed by more than one thread?
- If yes, is it modified after initialization?
- Do you need atomic compound operations?
- Can you tolerate
nullkeys or values?
If the map is effectively immutable after construction, HashMap is fine. If it is mutable and shared, ConcurrentHashMap is the safe default. If you need null support, you must use HashMap with external synchronization or a custom wrapper.
For read-heavy workloads with occasional writes, ConcurrentHashMap shines because reads are lock-free. For write-heavy workloads, it still performs well because writes to different keys can proceed in parallel. If your workload has many threads writing to the same key, you will still face contention, but that is unavoidable regardless of the map implementation.
A Practical Example: Caching with ConcurrentHashMap
Consider a simple cache that stores computed values by key. With ConcurrentHashMap, you can implement a thread-safe cache with atomic computeIfAbsent:
public class Cache { private final ConcurrentHashMap<String, ExpensiveObject> cache = new ConcurrentHashMap<>(); public ExpensiveObject get(String key) { return cache.computeIfAbsent(key, this::loadFromSource); } private ExpensiveObject loadFromSource(String key) { // Simulate loading return new ExpensiveObject(key); } }
This ensures that loadFromSource is called at most once per key, even under concurrent access. If you used a HashMap with synchronizedMap, you would need to synchronize the entire method, which would block all reads and writes to the cache, reducing throughput.
The same pattern applies to other stateful components like connection pools, configuration stores, and event counters. ConcurrentHashMap provides the concurrency guarantees you need without forcing you to manage locks manually.
Final Consideration: When Not to Use ConcurrentHashMap
ConcurrentHashMap is not a universal replacement for HashMap. Its internal structure is more complex, which means it has a higher memory footprint per entry. If you have a very large map that is only accessed by one thread, the extra memory and overhead are wasted. Also, the weakly consistent iterators may be surprising if you expect fail-fast behavior.
Another limitation is the lack of null support. If your code relies on null values, you will need to refactor. Additionally, ConcurrentHashMap does not allow you to lock the entire map for a compound operation that spans multiple keys. For such operations, you would need to use external synchronization, which defeats the purpose of using a concurrent map.
In those cases, you might consider using a HashMap with explicit locking, or a specialized data structure like ConcurrentSkipListMap if you need ordering. The choice always depends on your specific concurrency requirements and the data you are storing.