Java Thread-Safe Collections: ConcurrentHashMap vs HashMap in Concurrent Environments

Overview of Java Thread-Safe Classes

Class Type Characteristics Description
Legacy Thread-Safe Classes Uses synchronized for thread safety Deprecated classes like Stack, Vector, Hashtable
Collections-Decorated Classes Wrapper classes using synchronized Thread-safe versions of standard collections
JUC Classes Uses CAS and multiple locks Recommended for concurrent applications

Legacy Thread-Safe Collections

Classes like Vector (Stack) and Hashtable use synchronized methods to ensure thread safety.

public synchronized V get(Object key) {
    Entry,?> tab[] = table;
    int hash = key.hashCode();
    int index = (hash & 0x7FFFFFFF) % tab.length;
    for (Entry,?> e = tab[index] ; e != null ; e = e.next) {
        if ((e.hash == hash) && e.key.equals(key)) {
            return (V)e.value;
        }
    }
    return null;
}

Colletcions-Decorated Thread-Safe Collections

These are wrapper classes that accept the original collection and add synchronized protection.

Collections.synchronizedCollection
Collections.synchronizedList
Collections.synchronizedMap
Collections.synchronizedSet
Collections.synchronizedNavigableMap
Collections.synchronizedNavigableSet
Collections.synchronizedSortedMap
Collections.synchronizedSortedSet

The implementation uses a mutex object to synchronize all method calls:

public static <K,V> Map<K,V> synchronizedMap(Map<K,V> m) {
    return new SynchronizedMap<>(m);
}

private static class SynchronizedMap<K,V> implements Map<K,V>, Serializable {
    private final Map<K,V> m;
    final Object mutex;
    
    SynchronizedMap(Map<K,V> m) {
        this.m = Objects.requireNonNull(m);
        mutex = this;
    }
    
    public V put(K key, V value) {
        synchronized (mutex) {return m.put(key, value);}
    }
    
    public V get(Object key) {
        synchronized (mutex) {return m.get(key);}
    }
    // Other methods follow the same pattern
}

JUC Thread-Safe Classes

JUC provides three categories of thread-safe classes:

  • Blocking Classes: Use locks (usually ReentrantLock) and provide blocking methods
  • CopyOnWrite Classes: Use copy-on-write mechanism for thread safety
  • Concurrent Classes: High-performance concurrent containers using CAS and multiple locks

Concurrent classes have weak consistency:

  • Iterators are weakly consistent (fail-safe)
  • Size operations may not be 100% accurate
  • Read operations may see stale data

ConcurrentHashMap Usage

Multi-threaded Counting Example

Scenario: Count letter occurrences across 26 files using multiple threads.

import java.io.*;
import java.util.*;
import java.util.concurrent.*;
import java.util.function.*;

class FileProcessor {
    static <V> void processFiles(Supplier<Map<String, V>> mapSupplier, 
                               BiConsumer<Map<String, V>, List<String>> processor) {
        Map<String, V> frequencyMap = mapSupplier.get();
        List<Thread> threads = new ArrayList<>();
        
        for (int i = 1; i <= 26; i++) {
            int fileIndex = i;
            Thread thread = new Thread(() -> {
                List<String> characters = readFileContent(fileIndex);
                processor.accept(frequencyMap, characters);
            });
            threads.add(thread);
        }
        
        threads.forEach(Thread::start);
        threads.forEach(thread -> {
            try {
                thread.join();
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        });
        
        System.out.println(frequencyMap);
    }
    
    public static List<String> readFileContent(int fileNumber) {
        List<String> characters = new ArrayList<>();
        try (BufferedReader reader = new BufferedReader(
                new InputStreamReader(new FileInputStream("./data/" + fileNumber + ".txt")))) {
            String line;
            while ((line = reader.readLine()) != null) {
                characters.add(line);
            }
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
        return characters;
    }
}

public class Main {
    public static void main(String[] args) {
        FileProcessor.processFiles(
            () -> new HashMap<String, Integer>(),
            (map, characters) -> {
                for (String ch : characters) {
                    Integer count = map.get(ch);
                    int newValue = (count == null) ? 1 : count + 1;
                    map.put(ch, newValue);
                }
            }
        );
    }
}

Result with HashMap (incorrect due to thread safety issues):

{a=199, b=200, c=197, d=196, e=198, f=199, g=198, h=197, i=199, j=198, k=198, l=198, m=199, n=197, o=195, p=199, q=197, r=199, s=200, t=198, u=200, v=200, w=199, x=200, y=196, z=199}

Incorrect Improvement (HashMap to ConcurrentHashMap)

FileProcessor.processFiles(
    () -> new ConcurrentHashMap<String, Integer>(),
    (map, characters) -> {
        for (String ch : characters) {
            Integer count = map.get(ch);
            int newValue = (count == null) ? 1 : count + 1;
            map.put(ch, newValue);
        }
    }
);

Result (still incorrect because ConcurrentHashMap only guarantees atomicity of individual operations):

{a=198, b=200, c=200, d=200, e=198, f=199, g=199, h=198, i=200, j=200, k=198, l=200, m=200, n=200, o=199, p=198, q=199, r=194, s=198, t=199, u=199, v=200, w=200, x=199, y=200, z=199}

Correct Improvement

FileProcessor.processFiles(
    () -> new ConcurrentHashMap<String, LongAdder>(),
    (map, characters) -> {
        for (String ch : characters) {
            LongAdder counter = map.computeIfAbsent(ch, key -> new LongAdder());
            counter.increment();
        }
    }
);

HashMap Fundamentals

Basic Structure

  • JDK 7: Array + Linked List
  • JDK 8: Array + Linked List + Red-Black Tree

Key HashMap Properties

public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable {
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // 16
    static final float DEFAULT_LOAD_FACTOR = 0.75f;
    static final int TREEIFY_THRESHOLD = 8;
    static final int UNTREEIFY_THRESHOLD = 6;
    static final int MIN_TREEIFY_CAPACITY = 64;
    
    static class Node<K,V> implements Map.Entry<K,V> {
        final int hash;
        final K key;
        V value;
        Node<K,V> next;
        
        Node(int hash, K key, V value, Node<K,V> next) {
            this.hash = hash;
            this.key = key;
            this.value = value;
            this.next = next;
        }
    }
    
    transient Node<K,V>[] table;
    transient int size;
    transient int modCount;
    int threshold;
    final float loadFactor;
}

JDK 7 HashMap Deadolck Issue

In JDK 7, HashMap could create circular linked lists during concurrent resizing due to head insertion method.

void transfer(Node[] newTable, boolean rehash) {
    int newCapacity = newTable.length;
    for (Node<K,V> e : table) {
        while (null != e) {
            Node<K,V> next = e.next;
            if (rehash) {
                e.hash = (e.key == null) ? 0 : hash(e.key);
            }
            int i = indexFor(e.hash, newCapacity);
            e.next = newTable[i];
            newTable[i] = e;
            e = next;
        }
    }
}

Scenario demonstrating deadlock:


Original chain: [1] (1,35)->(35,16)->(16,null)

Thread A executes to position 1, e = (1,35), next = (35,16), then suspends

Thread B performs resizing:
- Rehashes key 16 to position 17
- Rebuilds chain at position 1: (35,1)->(1,null)

Thread A resumes:
- e = (1,null), next = (35,1)
- First iteration: [1] (1,null)
- Second iteration: [1] (35,1)->(1,null)
- Third iteration: e = (1,null), e.next becomes 35, creating cycle
- Result: [1] (1,35)->(35,1)->(1,35)

JDK 8 resolved this by using tail insertion and maintaining head/tail references, but concurrent modifications can still cause data loss.

Tags: java ConcurrentHashMap hashmap thread-safety JUC

Posted on Tue, 18 Aug 2026 16:58:00 +0000 by Ali25m