HashMap在多线程环境下使用时存在死链问题。让我详细解释一下这个问题的原因以及如何规避:
问题原因分析
在JDK 7及之前版本的HashMap中,resize时采用的是头插法重新排列链表,这会导致链表顺序反转:
// JDK 7的resize代码片段voidtransfer(Entry[]newTable){Entry[]src=table;intnewCapacity=newTable.length;for(intj=0;j<src.length;j++){Entry<K,V>e=src[j];if(e!=null){src[j]=null;do{Entry<K,V>next=e.next;// 关键点:线程A在这里读取nextinti=indexFor(e.hash,newCapacity);e.next=newTable[i];// 头插法:新节点插入到链表头部newTable[i]=e;e=next;}while(e!=null);}}}死链产生过程:
- 线程A和B同时进行resize
- 线程A执行到
Entry<K,V> next = e.next;后被挂起 - 线程B完成resize,链表顺序被反转
- 线程A恢复执行,此时它的next指向的是反转前的下一个节点
- 形成环形链表,导致后续操作无限循环
解决方案
1. 使用线程安全的集合类
ConcurrentHashMap(推荐)
// Java 8+ 推荐ConcurrentHashMap<String,Object>concurrentMap=newConcurrentHashMap<>();// 指定初始容量和并发级别ConcurrentHashMap<String,Object>map=newConcurrentHashMap<>(16,0.75f,16);特点:
- JDK 8+采用CAS + synchronized,性能更好
- 分段锁(JDK 7)或桶级锁(JDK 8+)
- 读写操作大部分情况下不需要全局锁
Collections.synchronizedMap
Map<String,Object>synchronizedMap=Collections.synchronizedMap(newHashMap<>());特点:对整个map加锁,性能较差
2. 显式加锁控制
// 使用ReentrantLockpublicclassSafeHashMap<K,V>{privatefinalMap<K,V>map=newHashMap<>();privatefinalReentrantLocklock=newReentrantLock();publicVput(Kkey,Vvalue){lock.lock();try{returnmap.put(key,value);}finally{lock.unlock();}}publicVget(Kkey){lock.lock();try{returnmap.get(key);}finally{lock.unlock();}}}3. 使用其他并发数据结构
CopyOnWrite(适合读多写少)
CopyOnWriteArrayList<String>list=newCopyOnWriteArrayList<>();CopyOnWriteArraySet<String>set=newCopyOnWriteArraySet<>();ConcurrentLinkedQueue/Deque
ConcurrentLinkedQueue<String>queue=newConcurrentLinkedQueue<>();ConcurrentLinkedDeque<String>deque=newConcurrentLinkedDeque<>();4. 使用读写锁(ReadWriteLock)
publicclassReadWriteMap<K,V>{privatefinalMap<K,V>map=newHashMap<>();privatefinalReadWriteLocklock=newReentrantReadWriteLock();publicVput(Kkey,Vvalue){lock.writeLock().lock();try{returnmap.put(key,value);}finally{lock.writeLock().unlock();}}publicVget(Kkey){lock.readLock().lock();try{returnmap.get(key);}finally{lock.readLock().unlock();}}}版本差异说明
JDK 8+的改进
JDK 8中的HashMap在resize时:
- 保持了链表元素的顺序(尾插法)
- 但仍然不是线程安全的,只是减少了死链的概率
- 仍可能出现数据丢失、size计算错误等问题
// JDK 8使用尾插法if(loTail!=null){loTail.next=null;newTab[j]=loHead;// 保持原顺序}最佳实践建议
1. 单线程环境
- 使用
HashMap(性能最好)
2. 多线程环境
- 读写都频繁:
ConcurrentHashMap - 写少读多:
CopyOnWrite系列 - 需要强一致性:
Collections.synchronizedMap或显式加锁 - 高并发计数:
LongAdder、ConcurrentHashMap的原子操作
3. 特定场景
// 统计计数ConcurrentHashMap<String,LongAdder>counter=newConcurrentHashMap<>();counter.computeIfAbsent(key,k->newLongAdder()).increment();// 缓存实现ConcurrentHashMap<String,Future<T>>cache=newConcurrentHashMap<>();4. 代码示例:线程安全的缓存
publicclassThreadSafeCache<K,V>{privatefinalConcurrentHashMap<K,V>cache=newConcurrentHashMap<>();privatefinalConcurrentHashMap<K,Future<V>>futures=newConcurrentHashMap<>();publicVget(Kkey,Callable<V>loader)throwsException{Vvalue=cache.get(key);if(value==null){Future<V>future=futures.computeIfAbsent(key,k->CompletableFuture.supplyAsync(()->{try{returnloader.call();}catch(Exceptione){thrownewRuntimeException(e);}}));value=future.get();cache.put(key,value);futures.remove(key);}returnvalue;}}总结
| 方案 | 线程安全 | 性能 | 适用场景 |
|---|---|---|---|
| HashMap | 不安全 | 最高 | 单线程环境 |
| ConcurrentHashMap | 安全 | 高 | 高并发读写 |
| Collections.synchronizedMap | 安全 | 低 | 简单同步需求 |
| 显式加锁 | 安全 | 中 | 复杂同步逻辑 |
| CopyOnWrite | 安全 | 读高写低 | 读多写少 |
核心建议:
- 在多线程环境下,永远不要使用HashMap
- 首选
ConcurrentHashMap,它在绝大多数场景下都能提供良好的性能和线程安全