Java
HashMap
Multithreading
Concurrency
Code Fix

Given that HashMaps in jdk1.6 and above cause problems with multithreading, how should I fix my code

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

The fix is not to look for a "safe" HashMap version in a different JDK release. The real issue is that HashMap is not thread-safe, so if multiple threads read and write it concurrently, you need a concurrent collection or explicit synchronization.

What Is Actually Wrong with HashMap

The Java API documentation is explicit: HashMap is unsynchronized. If multiple threads access it and at least one thread modifies it structurally, access must be synchronized externally.

That means the problem is not unique to JDK 1.6 or above. Old blog posts often refer to resize and rehash bugs that became very visible under concurrent misuse, but the durable rule is simpler:

  • 'HashMap is fine for single-threaded use'
  • 'HashMap is not fine for concurrent mutation without synchronization'

A race can show up as lost updates, stale reads, corrupted internal state, or intermittent exceptions. The failure mode depends on timing, which is exactly why these bugs are painful to debug.

The First Fix: Use ConcurrentHashMap

For most shared mutable maps, ConcurrentHashMap is the right replacement.

java
1import java.util.Map;
2import java.util.concurrent.ConcurrentHashMap;
3
4public class CounterStore {
5    private final Map<String, Integer> counters = new ConcurrentHashMap<>();
6
7    public void increment(String key) {
8        counters.merge(key, 1, Integer::sum);
9    }
10
11    public int get(String key) {
12        return counters.getOrDefault(key, 0);
13    }
14}

This solves two problems at once:

  • the map itself is designed for concurrent access
  • the update uses merge, which makes the read-modify-write operation atomic

That second point matters. Replacing HashMap with ConcurrentHashMap but keeping non-atomic update logic is still buggy.

Why Simple get Plus put Is Not Enough

This code is incorrect even with ConcurrentHashMap:

java
1Integer current = counters.get(key);
2if (current == null) {
3    counters.put(key, 1);
4} else {
5    counters.put(key, current + 1);
6}

Two threads can still interleave and overwrite each other. Use atomic APIs such as:

  • 'putIfAbsent'
  • 'compute'
  • 'computeIfAbsent'
  • 'merge'

Here is the safe version again:

java
counters.merge(key, 1, Integer::sum);

When Collections.synchronizedMap Is Acceptable

If you truly need simple coarse-grained locking, you can wrap a HashMap:

java
1import java.util.Collections;
2import java.util.HashMap;
3import java.util.Map;
4
5Map<String, String> map = Collections.synchronizedMap(new HashMap<>());

This is valid, but it gives you a single lock around map access. That can be acceptable for low-contention code, but it usually scales worse than ConcurrentHashMap.

Iteration also needs extra care:

java
1synchronized (map) {
2    for (Map.Entry<String, String> entry : map.entrySet()) {
3        System.out.println(entry.getKey() + " = " + entry.getValue());
4    }
5}

Without the surrounding synchronized block, iteration over a synchronized wrapper is still unsafe.

Choose the Fix Based on Ownership

A useful design rule is:

  • if a single thread owns the map, keep HashMap
  • if many threads share it, prefer ConcurrentHashMap
  • if complex multi-step invariants must stay consistent, protect them with a higher-level lock

For example, if two maps must change together, a concurrent map alone is not enough. You probably need a lock around the full operation.

java
1private final Object lock = new Object();
2
3public void move(String key, Map<String, String> from, Map<String, String> to) {
4    synchronized (lock) {
5        String value = from.remove(key);
6        if (value != null) {
7            to.put(key, value);
8        }
9    }
10}

Concurrency bugs are rarely fixed by changing only the collection type. They are fixed by matching the data structure to the access pattern.

Avoid Misleading Workarounds

Developers sometimes try to "fix" the issue by:

  • upgrading the JDK without changing the code
  • assuming exceptions will reveal every race
  • wrapping only one method but not the entire compound operation

None of those solve the underlying correctness problem.

If the map is read-mostly and replaced wholesale, immutable snapshots can be even better than synchronization. But for a general shared mutable map, ConcurrentHashMap is the practical default.

Common Pitfalls

Replacing HashMap with ConcurrentHashMap but keeping non-atomic get then put logic still leaves race conditions in the program.

Using Collections.synchronizedMap and then iterating without synchronizing on the wrapper object is a frequent source of subtle bugs.

Assuming the issue exists only in one JDK version misses the real rule that HashMap has never been a thread-safe mutable map.

Over-synchronizing every operation can fix correctness but create unnecessary contention if a concurrent collection would do the job more cleanly.

Summary

  • 'HashMap is not thread-safe, regardless of JDK version.'
  • For shared mutable maps, ConcurrentHashMap is usually the correct replacement.
  • Use atomic methods like merge or computeIfAbsent, not get plus put.
  • 'Collections.synchronizedMap works, but it is a coarser-grained option and requires synchronized iteration.'
  • Choose the fix based on the whole concurrency pattern, not just on the collection name.

Course illustration
Course illustration

All Rights Reserved.