☕ Java

ConcurrentHashMap

ConcurrentHashMap is a thread-safe, high-performance hash map optimised for concurrent access. Unlike Hashtable and synchronised HashMap (which use a single lock), ConcurrentHashMap uses a more sophisticated concurrency mechanism — in Java 8+, it uses CAS (compare-and-swap) operations and per-bucket synchronisation — allowing multiple threads to read and write to different parts of the map simultaneously without blocking each other. It does not permit null keys or null values.

How ConcurrentHashMap Achieves Thread Safety

Java 7 ConcurrentHashMap divided the hash table into segments (default 16), each with its own lock. This allowed up to 16 concurrent writes to proceed simultaneously, one per segment. Java 8 redesigned the implementation entirely, removing segments in favour of a more fine-grained approach. In Java 8+, ConcurrentHashMap uses an array of buckets (same structure as HashMap). For reads, no locking is needed — the volatile keyword on the bucket entries ensures visibility. For writes to an empty bucket, a CAS (compare-and-swap) operation atomically installs the first entry without acquiring any lock. For writes to a non-empty bucket, only the specific bucket's head node is synchronised — other buckets are unaffected. This bucket-level locking means that in a map with 256 buckets, up to 256 concurrent writes can proceed simultaneously to different buckets. In practice, the concurrency level approaches the number of buckets, which is very high — far better than Hashtable's single lock or HashMap's no lock. ConcurrentHashMap's atomic compound operations — computeIfAbsent, merge, compute, and putIfAbsent — are implemented atomically. computeIfAbsent(key, mappingFunction) is guaranteed to call the mapping function at most once and to atomically install the result. This is important: with a synchronised HashMap, a naive if-absent-then-put pattern is not atomic and two threads could both compute the value simultaneously. ConcurrentHashMap's compute methods are the safe alternative. The no-null rule (both keys and values) is a deliberate design choice for concurrency: with HashMap, get() returning null is ambiguous (key absent or mapped to null), and this ambiguity could not be resolved safely without synchronisation in a concurrent context. By forbidding null, get() returning null unambiguously means the key is absent.
Java
// ── Thread-safe operations with no explicit locking: ─────────────────
ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();

// Safe from multiple threads simultaneously:
map.put("Alice", 90);
map.put("Bob",   85);

// Reads are lock-free:
Integer score = map.get("Alice");   // fast, no lock

// ── Atomic compound operations: ───────────────────────────────────────

// putIfAbsent — atomic: check-and-insert without race condition:
map.putIfAbsent("Carol", 78);    // inserts only if Carol not present
map.putIfAbsent("Alice", 100);   // no-op — Alice already present

// computeIfAbsent — compute value atomically if key absent:
// SAFE in concurrent context — mapping function called at most once:
Map<String, List<String>> groups = new ConcurrentHashMap<>();
groups.computeIfAbsent("team-a", k -> new CopyOnWriteArrayList<>()).add("Alice");
groups.computeIfAbsent("team-a", k -> new CopyOnWriteArrayList<>()).add("Bob");
// "team-a" list created once, both threads add to same list

// merge — atomic increment:
ConcurrentHashMap<String, Long> wordCount = new ConcurrentHashMap<>();
String[] words = {"cat", "dog", "cat", "bird", "cat", "dog"};
for (String word : words) {
    wordCount.merge(word, 1L, Long::sum);
}
System.out.println(wordCount);  // {cat=3, dog=2, bird=1}

// compute — atomically update:
map.compute("Alice", (k, v) -> v == null ? 1 : v + 10);

// ── No null keys or values: ───────────────────────────────────────────
try { map.put(null, 1);      } catch (NullPointerException e) { }
try { map.put("key", null);  } catch (NullPointerException e) { }
// NullPointerException for both — unlike HashMap

// Distinguish absent from null-mapped:
// map.get(key) returning null means key is ABSENT (no null values allowed)
if (map.get("Dave") == null) {
    System.out.println("Dave not in map");  // safe — null means absent
}

ConcurrentHashMap Bulk Operations and Stream Support

Java 8 added bulk operations to ConcurrentHashMap that execute on all entries concurrently. These operations support a parallelism threshold: if the map has fewer entries than the threshold, the operation runs sequentially; otherwise it runs with Fork/Join parallelism. A threshold of 1 means always parallel; Long.MAX_VALUE means always sequential. forEach(parallelismThreshold, action) iterates all entries. search(parallelismThreshold, searchFunction) returns the first non-null result of applying the function to entries. reduce(parallelismThreshold, transformer, reducer) maps each entry to a value and reduces them. These operations reflect the map's state at the time they start — they see entries that existed when the operation began. The size() method is not atomic — it is an approximation when concurrent modifications are occurring. For monitoring purposes, mappingCount() returns a long (better for very large maps). If you need an accurate count that is consistent with other operations, you need external synchronisation. ConcurrentHashMap is the standard concurrent map for applications where multiple threads read and write to a shared map. The typical pattern is a cache, a shared counter map, or a registration map where entries are added and looked up from many threads. For read-heavy workloads, ConcurrentHashMap's lock-free reads make it faster than anything using explicit locking. For write-heavy workloads with many threads updating the same keys, contention on those buckets may still be a bottleneck.
Java
ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();
// Populate with some data:
for (int i = 0; i < 1000; i++) {
    map.put("key-" + i, i);
}

// ── forEach with parallelism: ─────────────────────────────────────────
map.forEach(100, (k, v) -> {
    if (v > 990) System.out.println(k + "=" + v);
});
// 100 = parallelismThreshold: parallel if >100 entries (this map has 1000)

// ── search — find first entry matching condition: ─────────────────────
String found = map.search(100, (k, v) -> v > 995 ? k : null);
System.out.println("Found: " + found);  // some key with value > 995

// ── reduce — compute aggregate: ──────────────────────────────────────
int total = map.reduce(100,
    (k, v) -> v,          // transformer: get the value
    Integer::sum           // reducer: sum all values
);
System.out.println("Sum: " + total);   // sum of 0..999 = 499500

// ── mappingCount() — better than size() for large maps: ───────────────
long count = map.mappingCount();   // long — handles maps > Integer.MAX_VALUE
System.out.println("Count: " + count);  // 1000

// ── Common usage pattern — concurrent frequency counter: ──────────────
public class WordFrequencyCounter {
    private final ConcurrentHashMap<String, LongAdder> counts =
        new ConcurrentHashMap<>();

    public void count(String word) {
        // computeIfAbsent is atomic — LongAdder created once per word:
        counts.computeIfAbsent(word, k -> new LongAdder()).increment();
        // LongAdder provides thread-safe increment with low contention
    }

    public long getCount(String word) {
        LongAdder adder = counts.get(word);
        return adder == null ? 0 : adder.sum();
    }

    public Map<String, Long> getTopN(int n) {
        return counts.entrySet().stream()
            .sorted(Map.Entry.<String, LongAdder>comparingByValue(
                Comparator.comparingLong(LongAdder::sum)).reversed())
            .limit(n)
            .collect(Collectors.toMap(
                Map.Entry::getKey,
                e -> e.getValue().sum(),
                (a, b) -> a,
                LinkedHashMap::new));
    }
}

Related Topics in Collections Framework

HashMap
HashMap is the most widely used Map implementation in Java. It stores key-value pairs in a hash table, providing average O(1) performance for get, put, remove, and containsKey operations. Keys are stored in no guaranteed order. HashMap permits one null key and multiple null values. It is not thread-safe. Understanding how HashMap uses hash codes, handles collisions, and resizes is foundational knowledge for writing efficient Java code.
LinkedHashMap
LinkedHashMap extends HashMap and maintains a doubly-linked list running through all its entries, preserving the order in which entries were inserted. Iteration over a LinkedHashMap always returns entries in insertion order. Optionally, it can be constructed in access-order mode where entries are ordered by most-recently accessed, making it the perfect foundation for implementing a Least Recently Used (LRU) cache. LinkedHashMap has slightly higher memory overhead than HashMap due to the extra linked list pointers.
TreeMap
TreeMap is a Red-Black tree implementation of the NavigableMap interface. It stores key-value pairs in sorted order — either the natural ordering of keys (requiring them to implement Comparable) or a custom Comparator provided at construction. All basic operations (get, put, remove, containsKey) are O(log n). TreeMap provides rich navigation operations: finding the closest key, extracting sub-maps, and headMap/tailMap views.
Hashtable
Hashtable is a legacy hash table implementation that predates the Collections Framework. Like Vector for lists, Hashtable was Java's original hash map from Java 1.0. It is thread-safe because all its methods are synchronised, but it does not permit null keys or null values. Hashtable has been retrofitted to implement the Map interface but should never be used in new code — HashMap is the non-concurrent replacement and ConcurrentHashMap is the concurrent replacement.