HashMap不是线程安全的,多线程下put、get或扩容可能引发死循环、数据丢失或NullPointerException;ConcurrentHashMap通过分桶加锁、CAS及volatile实现高并发安全;Collections.synchronizedMap则全局串行化,适用极低并发场景。

HashMap在多线程环境下会出什么问题
直接说结论:HashMap 不是线程安全的,多线程同时执行 put、get 或扩容操作时,可能触发死循环、数据丢失、甚至 NullPointerException。典型表现是 CPU 占用飙升(JDK 7 中链表成环导致遍历死循环),或返回 null 值(JDK 8 中并发 put 覆盖未同步的节点)。
根本原因在于:内部数组扩容(resize)和节点迁移过程没有同步控制,多个线程同时操作同一桶(bucket)时,链表/红黑树结构会被错误重排。
- JDK 7:扩容时头插法 + 多线程并发迁移 → 链表成环 →
get()死循环 - JDK 8:虽改用尾插法避免成环,但
putVal()中的 CAS 和 synchronized 粒度仅限单个桶,仍无法保证跨桶操作(如 resize 触发全局 rehash)的原子性 - 即使只读(全
get),若期间发生扩容,也可能读到不一致的中间状态(如新旧 table 并存时未完全迁移的节点)
ConcurrentHashMap 是怎么解决的
ConcurrentHashMap 的设计目标不是“全程加锁”,而是分段控制 + 无锁化优化。JDK 8 起彻底放弃分段锁(Segment),改用更细粒度的 synchronized 锁住单个桶(Node 链表头节点),配合 CAS 操作实现高并发写入。
关键机制包括:
立即学习“Java免费学习笔记(深入)”;
- 初始化时懒加载
table,避免无谓内存占用 - 扩容时允许多线程协作迁移(
transfer()),每个线程负责一部分桶,通过sizeCtl控制协调 - 读操作(
get)完全无锁,依赖volatile语义保证可见性;写操作仅对冲突桶加锁,不影响其他桶 - 当链表长度 ≥ 8 且
table.length ≥ 64时自动转为红黑树,降低查找时间复杂度
ConcurrentHashMapmap = new ConcurrentHashMap<>(); map.put("a", 1); // 锁住 hash 对应的桶头节点 map.get("b"); // 不加锁,直接 volatile 读
什么时候该用 Collections.synchronizedMap
Collections.synchronizedMap(new HashMap()) 是最简单的线程安全包装方案,但它把所有方法(put、get、size、keySet().iterator())都串行化了——每次调用都锁住整个 map 对象。
ECTouch是上海商创网络科技有限公司推出的一套基于 PHP 和 MySQL 数据库构建的开源且易于使用的移动商城网店系统!应用于各种服务器平台的高效、快速和易于管理的网店解决方案,采用稳定的MVC框架开发,完美对接ecshop系统与模板堂众多模板,为中小企业提供最佳的移动电商解决方案。ECTouch程序源代码完全无加密。安装时只需将已集成的文件夹放进指定位置,通过浏览器访问一键安装,无需对已有
适用场景非常有限:
- 读多写少,且并发量极低(比如配置缓存,启动后基本只读)
- 需要保持
HashMap的行为兼容性(如允许nullkey/value,而ConcurrentHashMap不允许nullkey 或 value) - 迭代时必须保证强一致性(
synchronizedMap的iterator()是同步的,而ConcurrentHashMap迭代器是弱一致性的)
注意:keySet()、values()、entrySet() 返回的集合本身不带同步,必须手动同步外部迭代:
MapsyncMap = Collections.synchronizedMap(new HashMap<>()); // ❌ 错误:迭代时不加锁 for (String key : syncMap.keySet()) { ... } // ✅ 正确:显式同步整个 map 实例 synchronized (syncMap) { for (String key : syncMap.keySet()) { ... } }
别踩这些坑
很多看似“安全”的写法其实掩盖了并发风险:
- 用
final修饰HashMap字段 ≠ 线程安全(只是引用不可变,内部状态仍可被并发修改) -
ConcurrentHashMap的computeIfAbsent是原子的,但传入的 mappingFunction 内部若访问共享变量,仍需自行同步 -
size()在ConcurrentHashMap中是估算值(各段计数异步汇总),不能用于条件判断(如if (map.size() == 0)应改用isEmpty()) - 混合使用
put和replace时,要注意replace(key, oldVal, newVal)的三参数版本才是原子比较并替换,两参数版本不检查旧值
最常被忽略的一点:线程安全只解决数据结构本身的并发问题,不解决业务逻辑的竞态。比如“先查再 put”这种模式,无论用哪种 Map,都必须用 computeIfAbsent 或外部锁兜底。









