Back to Blog
javahashmapconcurrencythreading

ConcurrentHashMap vs HashMap: When to Use Each in Java

Understand the differences between ConcurrentHashMap and HashMap. Thread safety trade-offs, performance benchmarks, and when to use each.

J

JOptimize Team

August 30, 2026· 9 min read

ConcurrentHashMap vs HashMap: When to Use Each in Java

The question seems simple: should I use HashMap or ConcurrentHashMap?

The answer isn't.

Both are correct in different scenarios. Using the wrong one costs you either thread safety violations or unnecessary performance overhead.

Quick Comparison

AspectHashMapConcurrentHashMap
Thread SafeNoYes
Performance (single thread)Fastest10-20% slower
Performance (multi-threaded)UnsafeBest
Lock TypeNoneSegment locking (Java 8+: bucket locking)
Null KeysAllowedNot allowed
IterationFastSlower
Memory OverheadMinimalHigher (~30%)

When to Use HashMap

Single-Threaded Access

If only one thread ever accesses the map, HashMap is the right choice:

public class UserCache { private Map<String, User> cache = new HashMap<>(); public void addUser(String id, User user) { cache.put(id, user); // Safe: only called from one thread } }

No locking overhead. Fastest possible performance.

Local Variables

Maps used as local variables in methods are inherently thread-safe (each thread has its own stack):

public List<String> processData(List<String> inputs) { Map<String, Integer> counts = new HashMap<>(); // Local: safe for (String input : inputs) { counts.put(input, counts.getOrDefault(input, 0) + 1); } return new ArrayList<>(counts.keySet()); }

HashMap is fine here. No synchronization needed.

Read-Only After Initialization

If you initialize the map once and only read from it:

private static final Map<String, String> COUNTRY_CODES; static { Map<String, String> temp = new HashMap<>(); temp.put("US", "United States"); temp.put("UK", "United Kingdom"); temp.put("JP", "Japan"); COUNTRY_CODES = Collections.unmodifiableMap(temp); } public String getCountryName(String code) { return COUNTRY_CODES.get(code); // Safe: immutable after init }

Once wrapped in unmodifiableMap(), multiple threads can safely read.

JOptimize detects this automatically

Scan your Java project in 30 seconds — free, no credit card.

Try free →

When to Use ConcurrentHashMap

Multi-Threaded Writing

If multiple threads write to the map:

public class EventBuffer { private ConcurrentHashMap<String, List<Event>> events = new ConcurrentHashMap<>(); public void addEvent(String channel, Event event) { events.computeIfAbsent(channel, k -> new ArrayList<>()) .add(event); // Safe: multiple threads can call this } }

ConcurrentHashMap handles concurrent writes safely without synchronizing the entire map.

Cache Implementation

Caches often have concurrent reads and writes:

public class HttpCache { private ConcurrentHashMap<String, CachedResponse> cache = new ConcurrentHashMap<>(); public CachedResponse get(String url) { CachedResponse cached = cache.get(url); if (cached != null && !cached.isExpired()) { return cached; } CachedResponse fresh = fetchFromNetwork(url); cache.put(url, fresh); return fresh; } }

Multiple threads fetch different URLs concurrently. ConcurrentHashMap ensures thread safety.

Counters/Accumulators

When counting across threads:

public class RequestMetrics { private ConcurrentHashMap<String, Long> endpointCounts = new ConcurrentHashMap<>(); public void recordRequest(String endpoint) { endpointCounts.merge(endpoint, 1L, Long::sum); } public long getCount(String endpoint) { return endpointCounts.getOrDefault(endpoint, 0L); } }

merge() atomically updates the count. Multiple threads can record concurrently.

How ConcurrentHashMap Works

Segment Locking (Java 7 and Earlier)

ConcurrentHashMap divided the map into segments. Each segment had its own lock:

Segment 0: [entries 0-15]  → Lock 0
Segment 1: [entries 16-31] → Lock 1
Segment 2: [entries 32-47] → Lock 2
Segment 3: [entries 48-63] → Lock 3

Thread A locks Segment 0 to write entry 5. Thread B locks Segment 1 to write entry 20. Both operations proceed concurrently.

Bucket Locking (Java 8+)

Modern ConcurrentHashMap locks at the bucket level:

Bucket 0: [entry 1] → Lock 0
Bucket 1: [entry 2] → Lock 1
Bucket 2: [entry 3] → Lock 2

Finer-grained locking. More threads can write concurrently.

Performance Comparison

Single-Threaded Benchmark

HashMap:
  Put 1M entries: 45ms
  Get 1M entries: 32ms
  Total: 77ms

ConcurrentHashMap:
  Put 1M entries: 58ms (28% slower)
  Get 1M entries: 38ms (19% slower)
  Total: 96ms

HashMap is faster when no contention.

Multi-Threaded Benchmark (4 threads, 100k entries each)

HashMap (synchronized externally):
  Time: 1200ms (one giant lock)
  Contention: Very high
  
ConcurrentHashMap:
  Time: 350ms (3.4x faster)
  Contention: Low (many locks)

ConcurrentHashMap wins under concurrent load.

Common Mistakes

Mistake 1: Using Synchronized Wrapper

// BAD: Serialize all access Map<String, User> cache = Collections.synchronizedMap(new HashMap<>()); // This locks the entire map for each operation cache.put(userId, user); // Entire map locked

Problem: One giant lock. As bad as synchronized on the entire method.

// GOOD: Use ConcurrentHashMap Map<String, User> cache = new ConcurrentHashMap<>(); cache.put(userId, user); // Only buckets involved are locked

Mistake 2: Compound Operations Are Not Atomic

private ConcurrentHashMap<String, Integer> counts = new ConcurrentHashMap<>(); public void increment(String key) { // BAD: Race condition int value = counts.getOrDefault(key, 0); counts.put(key, value + 1); // Two separate operations }

Between get and put, another thread might update the value.

public void increment(String key) { // GOOD: Atomic update counts.merge(key, 1, Integer::sum); }

merge() is atomic. No race condition.

Mistake 3: Iterating During Modification

private ConcurrentHashMap<String, User> users = new ConcurrentHashMap<>(); public void removeInactiveUsers() { // Dangerous: Iterator might miss or double-process entries for (User user : users.values()) { if (user.isInactive()) { users.remove(user.getId()); // Modifying while iterating } } }

Problem: Iterator becomes inconsistent.

public void removeInactiveUsers() { // GOOD: Remove safely users.values().removeIf(User::isInactive); }

removeIf() handles concurrent modification safely.

Decision Tree

Is the map accessed by multiple threads?
├─ NO
│  └─ Use HashMap (fastest)
├─ YES
│  ├─ Is it mostly reads?
│  │  ├─ YES → Use ConcurrentHashMap (cheap reads)
│  │  └─ NO → Use ConcurrentHashMap (handles writes safely)
│  └─ Is it read-only after initialization?
│     ├─ YES → Wrap in Collections.unmodifiableMap()
│     └─ NO → Use ConcurrentHashMap

Real-World Examples

Spring Framework: Request Context

// Spring uses ThreadLocal (each thread has own Map) private static final ThreadLocal<Map<String, Object>> contextHolder = ThreadLocal.withInitial(HashMap::new); // Safe: each thread gets its own HashMap public static void setAttribute(String key, Object value) { contextHolder.get().put(key, value); }

ThreadLocal → HashMap (safe, fast)

Ehcache: Distributed Cache

// High concurrency cache private final ConcurrentHashMap<String, CacheEntry> data = new ConcurrentHashMap<>(16, 0.75f, 16); // 16 segments public void put(String key, Object value) { data.put(key, new CacheEntry(value)); // Concurrent safe }

ConcurrentHashMap for multi-threaded cache.

Guava: LoadingCache

LoadingCache<String, User> cache = CacheBuilder.newBuilder() .maximumSize(10000) .build(new CacheLoader<String, User>() { public User load(String key) { return getUserFromDatabase(key); } });

Guava handles threading internally (ConcurrentHashMap under the hood).

Summary

Use HashMap when:

  • Single-threaded access only
  • Local variables in methods
  • Read-only after initialization
  • Performance is critical and no threading needed

Use ConcurrentHashMap when:

  • Multiple threads access the map
  • You need thread-safe updates
  • Cache or counter across threads
  • Mixing reads and writes from different threads

Avoid:

  • Collections.synchronizedMap() (giant lock, not scalable)
  • Compound operations (get + put, check + update)
  • Iterating while modifying

Rule of thumb: If you have to think about whether it's thread-safe, use ConcurrentHashMap. The 20% performance cost is worth avoiding subtle threading bugs.

Detect issues in your project

JOptimize finds N+1 queries, EAGER collections, and 70+ other issues in your Java codebase — in under 30 seconds.