☕ 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

Collections Overview
The Java Collections Framework (JCF) is a unified architecture for representing and manipulating groups of objects. It provides a set of interfaces that define the operations a collection must support, a set of abstract classes that provide partial implementations, and a set of concrete implementations optimised for different use cases. Every Java developer uses collections daily — lists for sequences, sets for uniqueness, maps for key-value pairs, and queues for ordering — and choosing the right implementation for the right use case is one of the most fundamental practical skills in Java.
Iterable
Iterable<E> is the root interface of the Java Collections hierarchy. Any class that implements Iterable can be used in a for-each loop. It declares a single abstract method: iterator(), which returns an Iterator<E> that the for-each loop uses to traverse the elements. Implementing Iterable is all that is required to make a custom data structure work with Java's enhanced for loop, the Stream API, and any method that accepts an Iterable.
Collection Interface
Collection<E> is the root interface of the main collection hierarchy, extending Iterable<E>. It defines the common operations that all collection types must support: adding elements, removing elements, checking containment, querying size, clearing, converting to an array, and bulk operations. List, Set, and Queue all extend Collection. Map does not extend Collection because a map operates on key-value pairs rather than individual elements.
ArrayList
ArrayList is a resizable-array implementation of the List interface. It is the most commonly used collection in Java, providing dynamic sizing on top of a standard array. Elements are stored in contiguous memory, enabling O(1) random access by index. When the internal array is full, ArrayList automatically allocates a larger array and copies all elements. ArrayList is not thread-safe and preserves insertion order, allowing duplicates.