Back to Blog
Java

Java ConcurrentHashMap: Thread-Safe Map for High Concurrency

java concurrenthashmap: Learn how Java's ConcurrentHashMap provides thread-safe map operations with high concurrency, including atomic methods, iteration behavior, and...

ConcurrentHashMapJava ConcurrencyThread SafetyJava CollectionsPerformance
Illustration of a ConcurrentHashMap with multiple threads accessing different bins concurrently, showing parallel read and write operations.

java concurrenthashmap requires a clear understanding of the core syntax, runtime behavior, and practical implementation patterns demonstrated in the examples below.

When multiple threads need to share a key-value store, a plain HashMap is not safe because its internal structure can be corrupted during concurrent resizing or rehashing. The traditional fix, Hashtable, synchronizes every method on a single lock, which serializes all access and destroys scalability. Java's ConcurrentHashMap addresses both problems by allowing concurrent reads and writes without a global lock, making it the default choice for thread-safe map operations in most production systems.

Why HashMap Is Not Enough for Concurrent Access

HashMap is designed for single-threaded use. Its put and get methods do not guarantee visibility of updates across threads, and concurrent modifications can leave the map in an inconsistent state. For example, two threads calling put simultaneously may trigger a resize that corrupts the bucket array, leading to infinite loops or lost entries. Even if you wrap a HashMap with Collections.synchronizedMap, every operation acquires the same monitor, so one thread's read blocks another thread's write. This approach works for small maps with low contention, but it does not scale when many threads access the map frequently.

ConcurrentHashMap was introduced in Java 5 and redesigned in Java 8 to provide a higher level of concurrency. It allows multiple threads to read and write different parts of the map simultaneously, using fine-grained locking and lock-free reads.

How ConcurrentHashMap Achieves High Concurrency

In Java 8 and later, ConcurrentHashMap abandons the older segment-based locking in favor of a more granular approach. The map is divided into bins, each holding a linked list or tree of nodes. Writes to different bins can proceed in parallel because each bin has its own lock. Reads are lock-free; they rely on volatile reads and the happens-before guarantees established by the Java Memory Model. When a bin becomes too long, it is converted into a red-black tree to keep lookup time at O(log n) instead of O(n).

The concurrencyLevel constructor parameter, which was meaningful in Java 7 to set the number of segments, is now used only as a hint for initial sizing. The actual concurrency is determined by the number of bins, which grows as the map grows. This design keeps the overhead low for small maps while still supporting high throughput under heavy contention.

Basic Operations and Thread Safety

Using ConcurrentHashMap is similar to using a regular Map. The common methods like put, get, remove, and containsKey are thread-safe, but their individual guarantees differ from a Hashtable. For example, get returns the value associated with the key at the moment of the call, or null if the key is absent. There is no locking, so a concurrent put may or may not be visible depending on timing, but the map never becomes corrupted.

ConcurrentHashMap<String, Integer> scores = new ConcurrentHashMap<>(); scores.put("alice", 90); scores.put("bob", 85); Integer aliceScore = scores.get("alice");

This code is safe for concurrent use. You can call put from one thread and get from another without external synchronization. The map handles the internal coordination. However, you must be careful with compound actions. A sequence like if (!map.containsKey(key)) { map.put(key, value); } is not atomic; another thread could insert the same key between the check and the put, causing lost updates.

Atomic Methods for Conditional Updates

To avoid race conditions in compound operations, ConcurrentHashMap provides atomic methods that perform check-and-act in one step. The most commonly used are putIfAbsent, remove(key, value), replace(key, oldValue, newValue), and computeIfAbsent.

computeIfAbsent is particularly useful for caching. It atomically computes a value only if the key is not already present, avoiding the need for a separate check and put.

ConcurrentHashMap<String, List<String>> cache = new ConcurrentHashMap<>(); List<String> list = cache.computeIfAbsent("users", key -> new ArrayList<>()); list.add("john");

The lambda runs only when the key is absent. If two threads call computeIfAbsent with the same key, only one computes the value; the other thread waits for the computation to finish and then receives the same value. This is a powerful primitive for building thread-safe caches without external locking.

Similarly, putIfAbsent is useful for implementing a single-flight pattern where you want to avoid overwriting an existing value.

String oldValue = map.putIfAbsent("key", "newValue"); if (oldValue == null) { // key was absent, value inserted } else { // key already existed, oldValue is the previous value }

These atomic methods are the primary reason to choose ConcurrentHashMap over a synchronized map. They eliminate the need for external synchronization in many concurrent algorithms.

Iteration and Weak Consistency

Iterating over a ConcurrentHashMap is different from iterating over a HashMap in a single-threaded context. The iterators returned by keySet(), values(), and entrySet() are weakly consistent. They reflect the state of the map at some point since the iterator was created, but they do not throw ConcurrentModificationException if the map is modified during iteration. The iterator may or may not see the effects of concurrent modifications, and it is guaranteed to traverse each element at most once.

for (Map.Entry<String, Integer> entry : map.entrySet()) { System.out.println(entry.getKey() + " = " + entry.getValue()); }

This behavior is intentional. It allows a thread to iterate over a map while another thread updates it without locking the entire map. The tradeoff is that you cannot rely on seeing a consistent snapshot of the map at any single point in time. If you need a point-in-time snapshot, you can create a copy using new HashMap<>(map), but that requires copying the entire map and may be expensive.

Weak consistency also applies to the size() method. The returned size is an approximation; it may not reflect the exact number of entries if concurrent modifications are happening. For a precise count, you would need to lock the map or use a different data structure, but in most concurrent scenarios an approximate size is acceptable.

Performance Tradeoffs and When to Use It

ConcurrentHashMap is optimized for high concurrency, but it is not always the best choice. For single-threaded access, a plain HashMap is faster because it has no locking or atomicity overhead. If your application runs in a single thread or if you can guarantee that only one thread accesses the map at a time, stick with HashMap.

For multi-threaded access, ConcurrentHashMap outperforms Hashtable and Collections.synchronizedMap under contention because it allows parallel reads and writes to different bins. However, the performance advantage depends on the access pattern. If all threads are writing to the same key or to keys that hash to the same bin, the lock contention will still serialize those operations. In such cases, the map's concurrency is limited by the hash distribution of the keys.

Another tradeoff is memory footprint. ConcurrentHashMap uses more memory than HashMap because it maintains additional node structures and possibly tree bins. If memory is tight and concurrency is low, a synchronized HashMap might be more efficient.

Here is a quick comparison of the three common thread-safe map options:

ApproachLockingRead concurrencyWrite concurrencyAtomic operations
HashtableGlobal lockSerializedSerializedNone
Collections.synchronizedMapGlobal lockSerializedSerializedNone
ConcurrentHashMapPer-bin lockParallelParallel (different bins)Yes

Use ConcurrentHashMap when you need thread safety and expect multiple threads to read and write concurrently. Use a synchronized map when you need a simple thread-safe wrapper and the concurrency level is low, or when you need to lock the entire map for compound operations that are not covered by the atomic methods.

Common Pitfalls and Misconceptions

One common mistake is assuming that ConcurrentHashMap allows null keys or values. It does not. Unlike HashMap, which permits one null key and multiple null values, ConcurrentHashMap throws NullPointerException if you try to insert a null key or value. This is a deliberate design choice to avoid ambiguity in concurrent operations, where null is used as a sentinel for absent values.

Another pitfall is relying on the size() method for precise counts in a live system. As mentioned, the size is an approximation. If you need an exact count, you must either synchronize externally or maintain a separate counter, but that introduces its own concurrency issues.

Also, be careful when using computeIfAbsent with a long-running computation. The method blocks the bin's lock while the mapping function executes, so a slow computation can cause other threads writing to the same bin to wait. In extreme cases, this can lead to thread starvation. If the computation is expensive, consider computing the value outside the map and then using putIfAbsent to avoid holding the lock during the computation.

Finally, remember that ConcurrentHashMap does not provide atomicity for compound operations that involve multiple keys. For example, transferring a value from one key to another requires external synchronization. The atomic methods only cover single-key operations. If you need multi-key transactions, you should use a different concurrency control mechanism, such as a database or a lock that covers all involved keys.

By understanding these behaviors and tradeoffs, you can use ConcurrentHashMap effectively in your concurrent Java applications, avoiding the performance pitfalls of global locking while maintaining thread safety.

java concurrenthashmap: Practical Usage and Code Examples | RYUSLOG DEV