Skip to content

HashMap线程安全问题与解决方案

引言

HashMap是非线程安全的,在多线程环境下使用会出现严重问题。本文将深入剖析HashMap的并发问题,对比Hashtable和ConcurrentHashMap,并给出实际应用建议。

HashMap的并发问题

问题一: 数据覆盖

多线程同时put可能导致数据丢失。

java
public class DataLossDemo {
    private static Map<String, String> map = new HashMap<>();
    
    public static void main(String[] args) throws InterruptedException {
        // 启动10个线程同时put
        Thread[] threads = new Thread[10];
        for (int i = 0; i < 10; i++) {
            final int index = i;
            threads[i] = new Thread(() -> {
                for (int j = 0; j < 1000; j++) {
                    map.put("key-" + index + "-" + j, "value");
                }
            });
            threads[i].start();
        }
        
        // 等待所有线程完成
        for (Thread t : threads) {
            t.join();
        }
        
        System.out.println("期望: 10000, 实际: " + map.size());
        // 输出: 实际值通常小于10000 (数据丢失)
    }
}

原因分析:

mermaid
graph TB
    A[线程A和B同时put] --> B[都计算到index=3]
    B --> C[都读取table-3-=null]
    C --> D[线程A: table-3-=nodeA]
    C --> E[线程B: table-3-=nodeB]
    E --> F[nodeA被覆盖丢失]
    
    style A fill:#4A90E2,color:#fff
    style B fill:#F5A623,color:#fff
    style C fill:#F5A623,color:#fff
    style D fill:#7ED321,color:#fff
    style E fill:#E74C3C,color:#fff
    style F fill:#E74C3C,color:#fff

问题二: 死循环 (JDK 1.7)

JDK 1.7的头插法在并发扩容时可能形成环形链表。

java
// JDK 1.7的transfer方法(简化版)
void transfer(Entry[] newTable) {
    Entry[] src = table;
    for (int j = 0; j < src.length; j++) {
        Entry<K,V> e = src[j];
        if (e != null) {
            src[j] = null;
            do {
                Entry<K,V> next = e.next;
                int i = indexFor(e.hash, newTable.length);
                e.next = newTable[i];  // 头插法
                newTable[i] = e;
                e = next;
            } while (e != null);
        }
    }
}

形成环形链表的过程:

mermaid
graph TB
    A[初始: A → B] --> B[线程1扩容<br/>e=A, next=B]
    B --> C[线程2扩容完成<br/>B → A]
    C --> D[线程1继续<br/>A.next=B]
    D --> E[B.next=A]
    E --> F[形成环: A ⇄ B]
    F --> G[CPU 100%死循环]
    
    style A fill:#4A90E2,color:#fff
    style B fill:#F5A623,color:#fff
    style C fill:#F5A623,color:#fff
    style D fill:#E74C3C,color:#fff
    style E fill:#E74C3C,color:#fff
    style F fill:#E74C3C,color:#fff
    style G fill:#E74C3C,color:#fff

JDK 1.8的解决:

java
// JDK 1.8改为尾插法,避免死循环
Node<K,V> loHead = null, loTail = null;
do {
    next = e.next;
    if ((e.hash & oldCap) == 0) {
        if (loTail == null)
            loHead = e;
        else
            loTail.next = e;  // 尾插法
        loTail = e;
    }
} while ((e = next) != null);

问题三: Fast-fail机制

迭代过程中检测到并发修改会抛异常。

java
public class FastFailDemo {
    public static void main(String[] args) {
        Map<String, String> map = new HashMap<>();
        map.put("key1", "value1");
        map.put("key2", "value2");
        
        // 迭代时修改
        for (String key : map.keySet()) {
            map.put("key3", "value3");  // ConcurrentModificationException
        }
    }
}

三种Map的全面对比

HashMap vs Hashtable vs ConcurrentHashMap

mermaid
graph TB
    A[Map接口实现] --> B[HashMap<br/>非线程安全]
    A --> C[Hashtable<br/>线程安全-过时-]
    A --> D[ConcurrentHashMap<br/>线程安全-推荐-]
    
    B --> E[性能最高<br/>单线程使用]
    C --> F[synchronized全锁<br/>性能差]
    D --> G[分段锁/CAS<br/>性能优]
    
    style A fill:#4A90E2,color:#fff
    style B fill:#50E3C2,color:#fff
    style C fill:#E74C3C,color:#fff
    style D fill:#50E3C2,color:#fff
    style E fill:#50E3C2,color:#fff
    style F fill:#E74C3C,color:#fff
    style G fill:#50E3C2,color:#fff

详细对比表

特性HashMapHashtableConcurrentHashMap
线程安全
null key允许1个不允许不允许
null value允许多个不允许不允许
底层结构数组+链表+红黑树数组+链表数组+链表+红黑树
初始容量161116
扩容方式2倍2倍+12倍
锁机制全表锁分段锁/CAS
性能中高
推荐度单线程推荐不推荐多线程推荐

继承关系

mermaid
graph TB
    A[Map接口] --> B[AbstractMap抽象类]
    B --> C[HashMap]
    
    D[Dictionary抽象类] --> E[Hashtable]
    E -.实现.-> A
    
    A --> F[ConcurrentMap接口]
    F --> G[ConcurrentHashMap]
    
    style A fill:#4A90E2,color:#fff
    style B fill:#7ED321,color:#fff
    style C fill:#50E3C2,color:#fff
    style D fill:#E74C3C,color:#fff
    style E fill:#E74C3C,color:#fff
    style F fill:#9013FE,color:#fff
    style G fill:#50E3C2,color:#fff

Hashtable详解

锁机制

java
public class Hashtable<K,V> extends Dictionary<K,V> {
    
    // 所有方法都用synchronized
    public synchronized V get(Object key) {
        // ...
    }
    
    public synchronized V put(K key, V value) {
        // 不允许null
        if (value == null) {
            throw new NullPointerException();
        }
        // ...
    }
    
    public synchronized V remove(Object key) {
        // ...
    }
}

性能问题:

mermaid
graph LR
    A[线程1 put] --> B[获取锁]
    C[线程2 get] --> D[等待锁]
    E[线程3 remove] --> F[等待锁]
    
    B --> G[操作完成释放锁]
    G --> D
    
    style A fill:#7ED321,color:#fff
    style B fill:#7ED321,color:#fff
    style C fill:#E74C3C,color:#fff
    style D fill:#E74C3C,color:#fff
    style E fill:#E74C3C,color:#fff
    style F fill:#E74C3C,color:#fff

为什么不推荐?

  1. 性能差: 全表锁,任何操作都互斥
  2. 设计过时: JDK 1.0的产物
  3. 有更好替代: ConcurrentHashMap

ConcurrentHashMap详解

JDK 1.7: 分段锁

java
// JDK 1.7的Segment数组
final Segment<K,V>[] segments;

static final class Segment<K,V> extends ReentrantLock {
    transient volatile HashEntry<K,V>[] table;
    
    V put(K key, int hash, V value) {
        lock();  // 只锁当前Segment
        try {
            // put操作
        } finally {
            unlock();
        }
    }
}
mermaid
graph TB
    A[ConcurrentHashMap 1.7] --> B[Segment 0]
    A --> C[Segment 1]
    A --> D[Segment 2]
    A --> E[Segment 3]
    
    B --> F[独立的HashEntry数组]
    C --> G[独立的HashEntry数组]
    D --> H[独立的HashEntry数组]
    E --> I[独立的HashEntry数组]
    
    style A fill:#4A90E2,color:#fff
    style B fill:#7ED321,color:#fff
    style C fill:#7ED321,color:#fff
    style D fill:#7ED321,color:#fff
    style E fill:#7ED321,color:#fff

JDK 1.8: CAS + synchronized

java
public V put(K key, V value) {
    int hash = spread(key.hashCode());
    
    for (Node<K,V>[] tab = table;;) {
        Node<K,V> f;
        int n, i, fh;
        
        if (tab == null || (n = tab.length) == 0)
            tab = initTable();
        else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
            // CAS插入
            if (casTabAt(tab, i, null, new Node<K,V>(hash, key, value, null)))
                break;
        }
        else {
            // synchronized锁定头节点
            synchronized (f) {
                // 链表或树的插入
            }
        }
    }
}

优化点:

mermaid
graph LR
    A[优化策略] --> B[CAS无锁操作<br/>空桶插入]
    A --> C[synchronized细粒度锁<br/>只锁头节点]
    A --> D[红黑树优化<br/>长链表场景]
    
    style A fill:#4A90E2,color:#fff
    style B fill:#50E3C2,color:#fff
    style C fill:#50E3C2,color:#fff
    style D fill:#50E3C2,color:#fff

性能测试

并发性能对比

java
public class ConcurrentPerformanceTest {
    private static final int THREAD_COUNT = 10;
    private static final int OPERATIONS = 100000;
    
    public static void main(String[] args) throws Exception {
        System.out.println("=== HashMap (非线程安全) ===");
        testMap(new HashMap<>());
        
        System.out.println("\n=== Hashtable ===");
        testMap(new Hashtable<>());
        
        System.out.println("\n=== ConcurrentHashMap ===");
        testMap(new ConcurrentHashMap<>());
        
        System.out.println("\n=== Collections.synchronizedMap ===");
        testMap(Collections.synchronizedMap(new HashMap<>()));
    }
    
    static void testMap(Map<Integer, Integer> map) throws Exception {
        long start = System.currentTimeMillis();
        
        Thread[] threads = new Thread[THREAD_COUNT];
        for (int i = 0; i < THREAD_COUNT; i++) {
            final int threadId = i;
            threads[i] = new Thread(() -> {
                for (int j = 0; j < OPERATIONS; j++) {
                    int key = threadId * OPERATIONS + j;
                    map.put(key, key);
                    map.get(key);
                }
            });
            threads[i].start();
        }
        
        for (Thread t : threads) {
            t.join();
        }
        
        long time = System.currentTimeMillis() - start;
        System.out.println("耗时: " + time + "ms");
        System.out.println("实际大小: " + map.size());
    }
}

// 典型输出:
// === HashMap (非线程安全) ===
// 耗时: 450ms
// 实际大小: 985321 (数据丢失)

// === Hashtable ===
// 耗时: 2800ms
// 实际大小: 1000000

// === ConcurrentHashMap ===
// 耗时: 680ms
// 实际大小: 1000000

// === Collections.synchronizedMap ===
// 耗时: 2650ms
// 实际大小: 1000000

实际应用场景

场景1: 缓存系统

java
public class CacheService {
    // 多线程访问,使用ConcurrentHashMap
    private ConcurrentMap<String, Object> cache = new ConcurrentHashMap<>();
    
    public Object get(String key) {
        return cache.computeIfAbsent(key, k -> {
            // 缓存未命中,从数据库加载
            return loadFromDB(k);
        });
    }
    
    public void put(String key, Object value) {
        cache.put(key, value);
    }
    
    public void invalidate(String key) {
        cache.remove(key);
    }
    
    private Object loadFromDB(String key) {
        // 模拟数据库查询
        return "data_" + key;
    }
}

场景2: 计数器

java
public class ConcurrentCounter {
    private ConcurrentMap<String, AtomicLong> counters = new ConcurrentHashMap<>();
    
    public void increment(String key) {
        counters.computeIfAbsent(key, k -> new AtomicLong(0))
               .incrementAndGet();
    }
    
    public long get(String key) {
        AtomicLong counter = counters.get(key);
        return counter == null ? 0 : counter.get();
    }
    
    // 实际应用: 接口调用统计
    public void recordApiCall(String apiName) {
        increment("api:" + apiName);
    }
}

场景3: 会话管理

java
public class SessionManager {
    private ConcurrentMap<String, Session> sessions = new ConcurrentHashMap<>();
    private static final long SESSION_TIMEOUT = 30 * 60 * 1000; // 30分钟
    
    public Session getSession(String sessionId) {
        Session session = sessions.get(sessionId);
        if (session != null && !session.isExpired()) {
            session.updateAccessTime();
            return session;
        }
        return null;
    }
    
    public void createSession(String sessionId, Session session) {
        sessions.put(sessionId, session);
    }
    
    public void removeSession(String sessionId) {
        sessions.remove(sessionId);
    }
    
    // 定期清理过期session
    public void cleanupExpiredSessions() {
        long now = System.currentTimeMillis();
        sessions.entrySet().removeIf(entry -> 
            now - entry.getValue().getLastAccessTime() > SESSION_TIMEOUT
        );
    }
    
    static class Session {
        private long lastAccessTime;
        
        public void updateAccessTime() {
            this.lastAccessTime = System.currentTimeMillis();
        }
        
        public long getLastAccessTime() {
            return lastAccessTime;
        }
        
        public boolean isExpired() {
            return System.currentTimeMillis() - lastAccessTime > SESSION_TIMEOUT;
        }
    }
}

选择建议

决策流程

mermaid
graph TD
    A[需要使用Map] --> B{是否多线程?}
    
    B -->|否| C[HashMap]
    B -->|是| D{性能要求?}
    
    D -->|高| E[ConcurrentHashMap]
    D -->|低| F{JDK版本?}
    
    F -->|现代| E
    F -->|老旧| G[Collections.synchronizedMap]
    
    H[绝不使用] -.-> I[Hashtable]
    
    style A fill:#4A90E2,color:#fff
    style B fill:#F5A623,color:#fff
    style C fill:#50E3C2,color:#fff
    style D fill:#F5A623,color:#fff
    style E fill:#50E3C2,color:#fff
    style G fill:#9013FE,color:#fff
    style I fill:#E74C3C,color:#fff

使用建议表

场景推荐选择理由
单线程HashMap性能最优
多线程读多写少ConcurrentHashMap读操作几乎无锁
多线程写多ConcurrentHashMap分段锁/CAS优化
需要null key/valueHashMap其他不支持
遗留系统考虑升级到ConcurrentHashMap性能提升明显
新项目永远不用Hashtable已被淘汰

常见误区

误区1: 认为Hashtable是最安全的

java
// ✗ 错误认知
Hashtable<String, String> table = new Hashtable<>();

// 这样仍然不是原子操作!
if (!table.containsKey("key")) {
    table.put("key", "value");  // 可能被其他线程插入
}

// ✓ 正确做法
ConcurrentHashMap<String, String> map = new ConcurrentHashMap<>();
map.putIfAbsent("key", "value");  // 原子操作

误区2: 认为HashMap在单线程下会有问题

HashMap在单线程环境完全没问题,性能最优。

误区3: 过度使用ConcurrentHashMap

单线程环境使用ConcurrentHashMap是性能浪费。

总结

HashMap的线程安全问题及解决方案:

核心要点

  1. HashMap: 非线程安全,单线程使用
  2. Hashtable: 线程安全但性能差,不推荐
  3. ConcurrentHashMap: 线程安全且高性能,多线程首选

最佳实践

mermaid
graph LR
    A[最佳实践] --> B[单线程<br/>HashMap]
    A --> C[多线程<br/>ConcurrentHashMap]
    A --> D[绝不使用<br/>Hashtable]
    
    style A fill:#4A90E2,color:#fff
    style B fill:#50E3C2,color:#fff
    style C fill:#50E3C2,color:#fff
    style D fill:#E74C3C,color:#fff

选择正确的Map实现,是编写高质量并发代码的基础!

更新: 2025-12-04 17:34:58
原文: https://www.yuque.com/u22210564/zoxfmt/doc-03-10-hashmap-07

Java 后端面试知识库