Understand the differences between ConcurrentHashMap and HashMap. Thread safety trade-offs, performance benchmarks, and when to use each.
JOptimize Team
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.
| Aspect | HashMap | ConcurrentHashMap |
|---|---|---|
| Thread Safe | No | Yes |
| Performance (single thread) | Fastest | 10-20% slower |
| Performance (multi-threaded) | Unsafe | Best |
| Lock Type | None | Segment locking (Java 8+: bucket locking) |
| Null Keys | Allowed | Not allowed |
| Iteration | Fast | Slower |
| Memory Overhead | Minimal | Higher (~30%) |
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
// 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
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.
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.
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
// 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)
// 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.
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).
Use HashMap when:
Use ConcurrentHashMap when:
Avoid:
Collections.synchronizedMap() (giant lock, not scalable)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.
JOptimize finds N+1 queries, EAGER collections, and 70+ other issues in your Java codebase — in under 30 seconds.