HashMap不是线程安全的,多线程下put、get或扩容可能引发死循环、数据丢失或NullPointerException;ConcurrentHashMap通过分桶加锁、CAS及volatile实现高并发安全;Collections.synchronizedMap则全局串行化,适用极低并发场景。
直接说结论:HashMap 不是线程安全的,多线程同时执行 put、get 或扩容操作时,可能触发死循环、数据丢失、甚至 NullPointerException。典型表现是 CPU 占用飙升(JDK 7 中链表成环导致遍历死循环),或返回 null 值(JDK 8 中并发 put 覆盖未同步的节点)。
根本原因在于:内部数组扩容(resize)和节点迁移过程没有同步控制,多个线程同时操作同一桶(bucket)时,链表/红黑树结构会被错误重排。
get() 死循环putVal() 中的 CAS 和 synchronized 粒度仅限单个桶,仍无法保证跨桶操作(如 resize 触发全局 rehash)的原子性get),若期间发生扩容,也可能读到不一致的中间状态(如新旧 table 并存时未完全迁移的节点)ConcurrentHashMap 的设计目标不是“全程加锁”,而是分段控制 + 无锁化优化。JDK 8 起彻底放弃分段锁(Segment),改用更细粒度的 synchronized 锁住单个桶(Node 链表头节点),配合 CAS 操作实现高并发写入。
关键机制包括:
table,避免无谓内存占用
transfer()),每个线程负责一部分桶,通过 sizeCtl 控制协调get)完全无锁,依赖 volatile 语义保证可见性;写操作仅对冲突桶加锁,不影响其他桶table.length ≥ 64 时自动转为红黑树,降低查找时间复杂度ConcurrentHashMapmap = new ConcurrentHashMap<>(); map.put("a", 1); // 锁住 hash 对应的桶头节点 map.get("b"); // 不加锁,直接 volatile 读
Collections.synchronizedMap(new HashMap()) 是最简单的线程安全包装方案,但它把所有方法(put、get、size、keySet().iterator())都串行化了——每次调用都锁住整个 map 对象。
适用场景非常有限:
HashMap 的行为兼容性(如允许 null key/value,而 ConcurrentHashMap 不允许 null key 或 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 或外部锁兜底。