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:#fffJDK 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详细对比表
| 特性 | HashMap | Hashtable | ConcurrentHashMap |
|---|---|---|---|
| 线程安全 | ✗ | ✓ | ✓ |
| null key | 允许1个 | 不允许 | 不允许 |
| null value | 允许多个 | 不允许 | 不允许 |
| 底层结构 | 数组+链表+红黑树 | 数组+链表 | 数组+链表+红黑树 |
| 初始容量 | 16 | 11 | 16 |
| 扩容方式 | 2倍 | 2倍+1 | 2倍 |
| 锁机制 | 无 | 全表锁 | 分段锁/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:#fffHashtable详解
锁机制
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为什么不推荐?
- 性能差: 全表锁,任何操作都互斥
- 设计过时: JDK 1.0的产物
- 有更好替代: 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:#fffJDK 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/value | HashMap | 其他不支持 |
| 遗留系统 | 考虑升级到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的线程安全问题及解决方案:
核心要点
- HashMap: 非线程安全,单线程使用
- Hashtable: 线程安全但性能差,不推荐
- 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