Concurrent Collections Interview Questions & Answers
This guide explains why Java 1.5 introduced concurrent collections and dives deep into the internal working of ConcurrentHashMap, including the critical Java 8 architectural overhaul.
1. Why were Concurrent Collections introduced?
Traditional collections like HashMap and ArrayList are not thread-safe. While Hashtable and Vector are thread-safe, they have severe limitations:
- Coarse-grained Locking: They lock the entire collection for every operation using
synchronizedonthis. Even read operations acquire the lock, meaning only one thread can access the collection at a time. - ConcurrentModificationException: If one thread iterates while another modifies, the fail-fast iterator throws
ConcurrentModificationExceptionโ even withHashtable.
The Solution: The java.util.concurrent package (Java 5) introduced collections designed for concurrency from the ground up:
| Collection | Replaces | Strategy |
|---|---|---|
ConcurrentHashMap | Hashtable / synchronized HashMap | Fine-grained locking + CAS |
CopyOnWriteArrayList | synchronized ArrayList | Snapshot on write |
ConcurrentLinkedQueue | synchronized LinkedList | Lock-free (CAS) |
BlockingQueue (variants) | Producer-consumer patterns | Lock-based with wait/notify |
2. HashMap vs. ConcurrentHashMap (Behavior Demo)
In a standard HashMap, modifying during iteration causes a crash:
Map<Integer, Integer> map = new HashMap<>();
map.put(1, 1); map.put(2, 2); map.put(3, 3);
for (Integer key : map.keySet()) {
if (key == 2) {
map.put(4, 4); // ConcurrentModificationException!
}
}
In ConcurrentHashMap, this is perfectly legal:
Map<Integer, Integer> map = new ConcurrentHashMap<>();
map.put(1, 1); map.put(2, 2); map.put(3, 3);
for (Integer key : map.keySet()) {
if (key == 2) {
map.put(4, 4); // Safe! Reflected in a "weakly consistent" manner
}
}
Weakly consistent iterators: ConcurrentHashMap iterators never throw ConcurrentModificationException. They reflect the state of the map at some point at or since the creation of the iterator. New entries may or may not be visible during iteration.
3. How does ConcurrentHashMap achieve better performance?
The implementation evolved dramatically from Java 7 to Java 8:
Java 7: Segment Locking Architecture
ConcurrentHashMap
โโโ Segment[0] (ReentrantLock + HashEntry[])
โโโ Segment[1] (ReentrantLock + HashEntry[])
โโโ ...
โโโ Segment[15] (ReentrantLock + HashEntry[])
- The map was divided into 16 segments (default), each being a mini-HashMap with its own
ReentrantLock. - A thread writing to Segment[3] only locks Segment[3]; another thread can simultaneously write to Segment[7].
- Maximum concurrency: Limited to the number of segments (default 16).
Java 8+: Node-Level CAS + Synchronized (Current)
Segments were completely removed. The new architecture:
ConcurrentHashMap
โโโ Node[] table (single flat array, like HashMap)
โโโ bucket[0] โ null (empty โ CAS insert)
โโโ bucket[1] โ Node โ Node โ Node (linked list โ synchronized on head node)
โโโ bucket[2] โ TreeBin โ TreeNode... (red-black tree โ synchronized on TreeBin)
โโโ ...
Three locking strategies based on bucket state:
-
Empty bucket โ CAS (no lock): If the target bucket is empty, use
compareAndSwapObjectto atomically insert the first node. Zero contention, zero blocking. -
Non-empty bucket โ
synchronizedon head node: Only the head node of the specific bucket is locked. Other buckets are completely unaffected. -
Tree bucket โ
synchronizedon TreeBin: If the bucket has been treeified (8+ entries), the TreeBin object is locked during modifications.
// Simplified Java 8+ put() logic
final V putVal(K key, V value) {
int hash = spread(key.hashCode());
for (Node<K,V>[] tab = table;;) {
Node<K,V> f = tabAt(tab, i); // volatile read
if (f == null) {
// CAS โ lock-free insert into empty bucket
if (casTabAt(tab, i, null, new Node<>(hash, key, value)))
break;
} else {
synchronized (f) { // Lock ONLY this bucket's head node
// Insert into linked list or tree
}
}
}
}
Result: Theoretical concurrency equals the number of buckets (thousands), not a fixed 16.
4. What is Concurrency Level?
The concurrency level parameter has different meanings across Java versions:
Java 7
It directly determined the number of Segments (default 16). Setting new ConcurrentHashMap<>(16, 0.75f, 64) created 64 segments, allowing 64 concurrent writers.
Java 8+
The concurrency level is only used as a sizing hint for the initial table capacity. It no longer creates segments because segments don't exist. The constructor comment in OpenJDK source says: "The value is used as a sizing hint."
// Java 8+: concurrencyLevel only affects initial table size
ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>(
initialCapacity, // 16
loadFactor, // 0.75
concurrencyLevel // Only a sizing hint, NOT segment count
);
Interview clarification: If the interviewer asks about "16 segments," clarify that this was the Java 7 model. Java 8+ uses a fundamentally different architecture with per-node synchronization.
5. Why are null keys/values not allowed in ConcurrentHashMap?
ConcurrentHashMap does not allow null keys or null values to avoid ambiguity in concurrent scenarios.
The Ambiguity Problem
In a single-threaded HashMap:
if (map.containsKey(key)) {
return map.get(key); // Could still be null (the value IS null)
}
This is safe because no other thread can intervene between containsKey() and get().
In a concurrent ConcurrentHashMap:
if (map.containsKey(key)) { // Thread A: true
// Thread B: map.remove(key); // REMOVED between these two calls!
return map.get(key); // Thread A: null โ but WHY?
// Is the value null? Or was the key removed by Thread B?
}
Doug Lea (the author) explicitly chose to disallow null to prevent this check-then-act race condition. With no nulls, get() returning null always means "key not found."
The putIfAbsent Case
// With null values allowed, this would be ambiguous:
map.putIfAbsent(key, value);
// Does "absent" mean "key not in map" or "key maps to null"?
6. Internal Operations: Get vs. Put
Read (get)
Lock-free. Uses volatile reads to ensure visibility:
// Simplified get() โ NO locking at all
V get(Object key) {
Node<K,V>[] tab = table; // volatile read of table reference
Node<K,V> e = tabAt(tab, (n - 1) & hash); // volatile read of bucket head
while (e != null) {
if (e.hash == hash && key.equals(e.key))
return e.val; // volatile read of value
e = e.next;
}
return null;
}
The val and next fields of Node are declared volatile, ensuring that any thread reading them sees the most recent write โ without any locking.
Write (put)
Uses the three-strategy approach described above:
- Calculate bucket index:
(table.length - 1) & hash - If bucket is empty โ CAS (atomic, no lock)
- If bucket has entries โ
synchronizedon the head node - If treeified โ
synchronizedon the TreeBin - After insertion, check if the bucket needs treeification (โฅ 8 entries)
- Check if the table needs resizing
Concurrent Resizing (Unique to ConcurrentHashMap)
Unlike HashMap where resizing is single-threaded, ConcurrentHashMap supports cooperative concurrent resizing. When one thread starts resizing, other threads that attempt to put() detect the resize in progress and help transfer entries to the new table. This distributes the O(n) resize cost across multiple threads.
7. What is "Lock Stripping"?
Lock stripping is the technique where a large data structure is broken into smaller pieces (strips), each with its own lock. This allows high levels of concurrency because threads only compete for locks if they are accessing the same "strip."
Evolution of the concept in ConcurrentHashMap
| Java Version | Strip Granularity | Max Concurrency |
|---|---|---|
| Java 5-7 | Segment (16 by default) | 16 concurrent writers |
| Java 8+ | Individual bucket (Node) | Thousands of concurrent writers |
Beyond ConcurrentHashMap
The same concept applies to other concurrent structures:
StampedLockprovides optimistic read locking where readers don't acquire any lock at allLongAdderuses cell-stripping to reduce contention on a counter: each thread increments its own cell, cells are summed only when the total is requested- Database engines use row-level locking instead of table-level locking (same principle)
