AlgoMaster Logo

Design Concurrent HashMap

Last Updated: February 1, 2026

Ashish

Ashish Pratap Singh

A hash map is one of the most used data structures in real systems, and making it work correctly under concurrency is harder than it looks.

Multiple threads can insert, remove, and resize at the same time, and a naive lock around the whole map quickly becomes a bottleneck. The goal of a concurrent hash map is to provide safe, scalable access with high throughput under contention.

In this chapter, we will design a concurrent hash map step by step. We will start with a simple coarse-grained lock, then evolve toward finer-grained locking and CAS based approach.

Problem Statement

What We're Building

A thread-safe HashMap that allows multiple threads to read and write concurrently without corrupting data or losing updates. The goal is to maximize throughput by allowing independent operations to proceed in parallel while maintaining correctness guarantees.

Required Operations

OperationDescriptionExpected Complexity
put(key, value)Insert or update a key-value pairO(1) average
get(key)Retrieve value for a keyO(1) average
remove(key)Delete a key-value pairO(1) average
putIfAbsent(key, value)Insert only if key doesn't existO(1) average
computeIfAbsent(key, func)Compute and insert if key missingO(1) average
size()Return number of entriesO(1) or O(segments)

Thread-Safety Requirements

  • Multiple threads can call get() simultaneously without blocking each other
  • Multiple threads can call put() on different keys simultaneously
  • Operations on the same key must be serialized to prevent lost updates
  • Compound operations like putIfAbsent() must be atomic
  • No thread should see partially constructed entries
  • Iterators should be weakly consistent (no ConcurrentModificationException)

Data Structure Fundamentals

Before adding concurrency, let's understand how a single-threaded HashMap works. This baseline is essential because concurrent solutions must preserve these mechanics while adding thread safety.

Core Concepts

  1. Bucket Array: A HashMap stores entries in an array of "buckets." Each bucket holds entries that hash to the same index.
  2. Hash Function: Keys are converted to array indices using hash(key) % capacity. A good hash function distributes keys uniformly across buckets.
  3. Collision Handling (Chaining): When multiple keys hash to the same bucket, entries are stored in a linked list (or tree for large buckets in Java 8+).
  4. Load Factor: The ratio of entries to buckets. When it exceeds a threshold (typically 0.75), the table resizes to maintain O(1) performance.

Single-Threaded Implementation

Internal Structure

The diagram shows a HashMap with 8 buckets. Keys A and I both hash to bucket 2 (hash % 8 = 2), forming a chain. Keys G and O share bucket 7. Empty buckets (0, 1, 3, 4, 6) contain null. This chaining approach handles collisions but creates the first concurrency challenge: two threads modifying the same chain can corrupt it.

Concurrency Challenges

Before diving into solutions, let's understand what makes HashMap challenging to parallelize. These challenges will guide our design decisions.

Challenge 1: Race Condition on Put

Consider two threads inserting keys that hash to the same bucket.

Both threads read the same head pointer (X). Thread 1 inserts A pointing to X. Thread 2 then overwrites the bucket head with B pointing to X. Node A is orphaned, and its entry is silently lost. This is a classic lost update problem.

Challenge 2: Read-Modify-Write Without Atomicity

The put operation is inherently a read-modify-write sequence:

  1. Read: Find the bucket, traverse to check if key exists
  2. Modify: Either update existing node's value or create new node
  3. Write: Update bucket head or node pointer

Without atomicity, another thread can interleave between steps 1 and 3, causing:

  • Lost updates (as shown above)
  • Duplicate entries for the same key
  • Corrupted linked list structure (cycles or broken chains)

Challenge 3: Resize Hazards

When the load factor threshold is exceeded, the table must double in capacity and rehash all entries. This is dangerous during concurrent access:

A reader using the old table reference might access entries that are being moved or have already been moved to the new table. The reader could return null for a key that actually exists.

Challenge 4: Compound Operations

putIfAbsent(key, value) must atomically:

  1. Check if key exists
  2. If not, insert the value
  3. Return the existing value or null

Without atomicity:

Both threads see the key as absent and both insert. One value is silently lost, and both callers believe their insert succeeded.

Consistency Model Requirements

PropertyRequirementWhy It Matters
AtomicitySingle-key operations must be atomicPrevents lost updates
VisibilityWrites must be visible to subsequent readsPrevents stale reads
LinearizabilityOperations appear to happen at a single instantEnables reasoning about concurrent behavior
ProgressAt least one thread makes progressPrevents system-wide stall

Approach 1: Coarse-Grained Locking

The simplest approach: wrap the entire HashMap with a single lock. Every operation acquires this lock, serializing all access.

Implementation

Analysis

PropertyStatusExplanation
Thread-safeYesSingle lock serializes all access
Deadlock-freeYesOnly one lock, no circular wait possible
Starvation-freeDependsReentrantLock with fairness=true guarantees it
ScalabilityPoorAll operations serialize, zero parallelism

Single Lock Bottleneck

The diagram shows the fundamental problem: all four threads must wait for a single lock, even though they're accessing completely independent keys. Threads 1 and 3 are both reading (could run in parallel) and accessing different keys (definitely could run in parallel). But the coarse-grained lock forces them to wait.

Pros

  • Simple to implement and verify correctness
  • No risk of deadlock or complex synchronization bugs
  • Compound operations are naturally atomic
  • Good when contention is low

Cons

  • Zero parallelism, even for independent keys
  • Read operations block each other
  • Throughput doesn't scale with cores
  • A slow operation blocks all other threads

When to Use:

  • Prototyping and testing
  • Low-contention scenarios (few threads, infrequent access)
  • When simplicity is more important than performance
  • As a correctness baseline for testing optimized implementations

Approach 2: Fine-Grained Locking (Lock Striping)

The key insight: most HashMap operations only touch one bucket. If we lock individual buckets (or groups of buckets), operations on different buckets can proceed in parallel. This is the approach used by Java 7's ConcurrentHashMap.

Strategy: Segment-Based Locking

Divide the hash table into segments, each with its own lock. The default is 16 segments. To access a key:

  1. Compute the segment: segment = hash(key) % NUM_SEGMENTS
  2. Acquire that segment's lock
  3. Perform the operation
  4. Release the lock

Threads 1, 2, and 3 access different segments and proceed in parallel. Thread 4 hashes to the same segment as Thread 1, so it waits. But overall concurrency is much higher than with a single lock.

Implementation

Handling Cross-Segment Operations

The size() method demonstrates the challenge of operations spanning multiple segments. We must lock all segments to get an accurate count. Without locking all, entries could be added or removed during counting, giving an inconsistent result.

Weakly Consistent Iteration

Iterating over a striped map can't guarantee a point-in-time snapshot without locking everything. Instead, we provide "weakly consistent" iteration:

  • May or may not reflect concurrent modifications
  • Never throws ConcurrentModificationException
  • Each element is returned at most once

This is acceptable for most use cases (logging, monitoring, debugging) where an approximate view is sufficient.

Analysis

PropertyStatusExplanation
Thread-safeYesEach segment protected by its own lock
Deadlock-freeYesSingle-segment ops use one lock; size() acquires in fixed order
ScalabilityGoodUp to 16x parallelism (or more with more segments)
ComplexityMediumMore code, segment management logic

Pros

  • Much better parallelism than coarse-grained locking
  • Operations on different segments don't block each other
  • Resize is per-segment, not global
  • Compound operations (putIfAbsent) are straightforward within a segment

Cons

  • size() requires locking all segments (expensive)
  • Memory overhead for multiple locks
  • Still serializes operations within the same segment
  • Iteration is weakly consistent, not snapshot

Approach 3: CAS-Based

Java 8 redesigned ConcurrentHashMap to eliminate segment locks for most operations. The key innovations:

  1. Lock-free reads: Use volatile reads, no locking for get()
  2. CAS for empty buckets: Insert into empty bucket using atomic CAS
  3. Synchronized on bucket head: Only lock when there's a collision
  4. Incremental resize: Spread resize work across threads using forwarding nodes

Key Insight

Most HashMap buckets are either empty or have very few entries. Java 8's approach optimizes for this:

  • Empty bucket? Use CAS to install the first node (lock-free)
  • Non-empty bucket? Synchronize on the first node (fine-grained, per-bucket lock)
  • Reading? Just read the volatile table reference and traverse (no lock)

CAS Insertion for Empty Buckets

The key insight: get() never locks. It relies on:

  • table being volatile (sees latest array reference)
  • Node.value being volatile (sees latest value)
  • Node objects being immutable once published (hash, key don't change)

Incremental Resize with Forwarding Nodes

When resizing, Java 8's ConcurrentHashMap doesn't stop the world. Instead:

  1. Create a new, larger table
  2. Mark buckets being migrated with "forwarding nodes"
  3. Spread migration work across threads
  4. Readers/writers that encounter forwarding nodes help migrate or access new table

Buckets 2, 3, and 7 are marked with forwarding nodes (FWD). Their entries have been moved to the new table. Buckets 1, 4, and 6 are still in the old table. A get(key) that hashes to bucket 2 sees the forwarding node and looks in the new table instead.

Atomic Compound Operations

putIfAbsent and computeIfAbsent must be atomic. In the CAS-based approach:

The synchronized block ensures the check-then-insert happens atomically. Between the existence check and the insert, no other thread can modify the bucket.

Analysis

PropertyStatusExplanation
Thread-safeYesCAS + synchronized ensures correctness
Lock-free readsYesNo locking for get()
ScalabilityExcellentPer-bucket locking, lock-free reads
ComplexityHighCAS loops, forwarding nodes, memory ordering

Solution Comparison

AspectCoarse-GrainedFine-Grained (Striping)CAS-Based (Java 8)
Read concurrency1 reader at a timeN readers (different segments)Unlimited (lock-free)
Write concurrency1 writer at a timeN writers (different segments)N writers (different buckets)
Lock overhead1 lock16 locks (default)Per-bucket sync, no global lock
Read performanceBlocked by writesBlocked by same-segment writesNever blocked
size() complexityO(1)O(segments) + locking allO(n) or approximate
Implementation complexitySimpleMediumHigh
Memory overheadMinimal16 locksPer-bucket potential lock
Resize strategyGlobal, stop-the-worldPer-segmentIncremental, concurrent

Decision Tree for Choosing Approach

Recommendation:

  • Interviews: Start with lock striping. It's conceptually clear, demonstrates understanding of concurrency trade-offs, and is what most interviewers expect. Mention the CAS-based approach as an optimization.
  • Production Java: Use java.util.concurrent.ConcurrentHashMap. It's battle-tested and implements the Java 8 optimizations.
  • Production C++: Use tbb::concurrent_hash_map from Intel TBB or folly::ConcurrentHashMap from Facebook.
  • Production Python: For true concurrency, use multiprocessing.Manager().dict() or database-backed solutions.

Complete Implementation

This section provides the production-ready lock striping implementation, which is most relevant for interviews. The code is complete, tested, and includes all methods discussed.