跳转到主内容
新派编程网 - 新潮编程理念,全新技术课堂!

Java多线程环境怎么用HashMap Java解决HashMap并发问题【技巧】

HashMap非线程安全,多线程下put/get或扩容易致死循环、数据丢失或ConcurrentModificationException;根本原因是JDK7链表头插法成环、JDK8迁移未加锁。 为什么直接在多线程里用
HashMap
会出问题
HashMap
不是线程安全的。多个线程同时执行
put()
、
get()
或触发扩容(
resize()
)时,可能引发死循环(JDK 7)、数据丢失、或
ConcurrentModificationException
。典型现象是程序卡死在某个线程、CPU 占用飙高,或者读到
null
值却没报错——这比崩溃更难排查。 根本原因是:扩容时链表头插法 + 多线程竞争导致环形链表(JDK 7),或节点迁移逻辑未加锁(JDK 8)。这不是偶发 bug,而是设计使然。 三种常用替代方案对比:选哪个取决于场景 不是所有并发场景都要上重量级锁。关键看读写比例、是否需要强一致性、以及是否允许短暂不一致:
ConcurrentHashMap
:绝大多数情况首选。JDK 8 起用
synchronized
+ CAS 替代分段锁,读操作无锁,写操作粒度细化到桶(bin)。适合读多写少,且不要求全局原子性操作的场景。
Collections.synchronizedMap(new HashMap())
:给整个 map 加一把大锁。读写都串行,吞吐量低,但能保证迭代时的线程安全(需手动同步
entrySet().iterator()
)。仅适用于并发不高、或已有代码改造成本低的过渡方案。
java.util.concurrent.locks.ReentrantLock
+ 手动保护
HashMap
:灵活性最高,但容易漏锁、死锁。除非你要做复合操作(比如“先 get 再 put”必须原子),否则不推荐——
ConcurrentHashMap
的
computeIfAbsent()
、
replace()
等方法已覆盖大部分需求。
ConcurrentHashMap
的几个关键用法和坑 它不是
HashMap
的简单线程安全版,行为有差异: 立即学习 “ Java免费学习笔记(深入) ”; Eclipse导入Android或其他的JAVA项目的正确方法 WORD版 本文档主要讲述的是Eclipse导入Android或其他的JAVA项目的正确方法;希望本文档会给有需要的朋友带来帮助;感兴趣的朋友可以过来看看 下载
size()
返回的是估算值,可能不准;要精确计数请用
mappingCount()
(返回
long
)。
containsKey()
和
containsValue()
是弱一致性:可能看不到其他线程刚写入的值,但不会抛异常或返回脏数据。 遍历时若用
for (Entry e : map.entrySet())
,底层是弱一致性快照,不会抛
ConcurrentModificationException
,但可能漏掉新插入项——这点和
HashMap
迭代器行为完全不同。 避免用
new ConcurrentHashMap(initialCapacity)
直接传初始容量。它的构造参数其实是「预估并发线程数」,真正控制桶数量的是
concurrencyLevel
(JDK 8 已弱化该参数,但传错仍影响初始化性能)。 什么时候真得自己加锁?一个典型例子 比如实现一个带过期时间的本地缓存,要求“查不到就加载并写入”,且加载过程耗时(如远程调用)。这时
ConcurrentHashMap.computeIfAbsent()
就很合适:
cache.computeIfAbsent(key, k -> { // 这个 lambda 只会在 key 不存在时执行一次,且整个过程原子 return loadFromRemote(k); });
但如果逻辑更复杂——例如要先检查某个标志位、再更新两个不同 key、最后发通知——那就超出
ConcurrentHashMap
能力范围了。此时建议用
ReentrantLock
包裹整个业务块,并注意锁粒度别太大(比如别锁住整个 map 实例,而应按 key 哈希分段锁)。 最常被忽略的一点:即使用了
ConcurrentHashMap
,如果 value 是可变对象(比如
ArrayList
),对 value 内部的修改仍然需要额外同步——容器线程安全 ≠ 元素线程安全。

相关文章