01
60 秒回答模板
可以从三个层次回答。第一,HashMap 不是线程安全的。它的 put、resize、链表或红黑树结构调整都没有同步保护,并发修改时可能导致覆盖写入、丢失更新、读到中间状态,甚至在旧版本实现中扩容时可能形成异常链表结构。因此只要存在并发写,就不能直接使用普通 HashMap。第二,常见安全方案要按场景选。最简单的是外部锁,例如 synchronized 或 ReentrantReadWriteLock 包住所有访问,优点是语义清楚,适合需要多个操作组成一个原子事务的场景,缺点是并发度低。Collections.synchronizedMap 是对 Map 方法加互斥锁的包装,单个方法调用是线程安全的,但复合操作仍然需要额外同步,迭代时也要手动在同一把锁内保护。更常用的是 ConcurrentHashMap,它针对并发访问设计,读操作大多无锁,写操作通常只锁局部桶或使用 CAS,扩容也做了并发协作,吞吐量通常明显好于整表加锁。第三,还要补充边界。ConcurrentHashMap 的单个 put、remove、get 是线程安全的,但“先判断再插入”这类复合逻辑不能用 containsKey + put 拆开写,应使用 putIfAbsent、computeIfAbsent、compute、merge 等原子方法。ConcurrentHashMap 的迭代器是弱一致的,不会抛 ConcurrentModificationException,但不保证看到遍历期间所有最新修改。如果读多写少,并且能接受更新通过整体替换发布,可以使用不可变 Map 快照、volatile 或 AtomicReference 持有新快照,或者 CopyOnWrite 思路,以牺牲写入成本和内存换取无锁读。最终选择取决于一致性要求和性能取舍:高并发通用场景优先 ConcurrentHashMap,复合事务用外部锁,不频繁更新的配置类数据用不可变快照。
考点 HashMap 风险
主线 按模型选方案
易错点 只说“HashMap 线程不安全,所以加 synchr…
02
深入解析
01 HashMap 风险
HashMap 的内部结构维护不是原子操作,并发 put、remove、resize 时没有互斥保护。多个线程同时修改同一个桶、同时触发扩容、同时更新 size 或链表节点,都可能产生丢失更新、覆盖写、结构不一致、读到旧值或中间状态等问题。因此线程安全问题不是单纯的可见性问题,还包括原子性和内部结构一致性问题。
02 按模型选方案
如果只是少量并发访问,可以用外部锁或 Collections.synchronizedMap,把所有访问串行化,简单但吞吐量有限。如果是高并发读写,通常使用 ConcurrentHashMap,它通过更细粒度的并发控制降低锁竞争,读多场景性能更好。如果数据更新很少、读取非常频繁,可以用不可变快照或 CopyOnWrite 思路,读线程始终读稳定版本,写线程构造新 Map 后一次性发布。
03 单操作和复合操作
线程安全容器只能保证它定义的单个操作安全,不自动保证业务上的多步逻辑安全。例如 containsKey 后再 put 之间可能被其他线程插入,仍然有竞态。需要使用 ConcurrentHashMap 的 putIfAbsent、computeIfAbsent、compute、merge 等原子方法,或者在外部用同一把锁包住整段业务逻辑。
04 遍历一致性
Collections.synchronizedMap 的迭代不是自动线程安全的,遍历期间需要手动持有包装对象的锁,否则可能出现并发修改异常或不一致结果。ConcurrentHashMap 的迭代器是弱一致的,遍历时不会因为并发修改直接失败,但也不承诺反映遍历期间的所有更新。若业务需要遍历期间的强一致快照,应复制一份快照或使用不可变快照方案。
05 性能取舍
整表加锁方案实现简单、一致性强,但并发度最低。ConcurrentHashMap 适合大多数高并发读写场景,但不能替代所有业务级事务锁。不可变快照和 CopyOnWrite 读性能优秀,读路径简单稳定,但写入需要复制数据,适合配置、路由表、规则表等写少读多数据,不适合频繁写入的大 Map。
03
易错点
- 只说“HashMap 线程不安全,所以加 synchronized”但不解释原因,回答会停留在结论层。
- 把 ConcurrentHashMap 说成所有操作都完全无锁。准确说法是读操作大多无锁,写操作会使用 CAS 和局部同步等机制。
- 认为使用线程安全 Map 后,containsKey + put 这样的复合逻辑也自动安全。容器线程安全不等于业务逻辑原子。
- 认为 Collections.synchronizedMap 的迭代自动安全。它只包装方法调用,遍历时需要手动在同一把锁下执行。
- 忽略 ConcurrentHashMap 的弱一致迭代语义,把它误认为能提供严格实时快照。
- 在读多写少场景只知道 ConcurrentHashMap,不知道不可变快照或 CopyOnWrite 可以简化读路径。
- 在需要跨多个数据结构保持一致时,只替换 Map 实现而不加外部锁,导致整体状态仍然有竞态。
- 把分段锁当成所有 JDK 版本 ConcurrentHashMap 的固定实现细节。更稳妥的表述是早期使用分段锁思想,现代实现主要是桶级同步和 CAS。
04
面试官追问
ConcurrentHashMap 一定能替代 synchronizedMap 吗?
不能。ConcurrentHashMap 吞吐量通常更好,但它的迭代是弱一致的,也不自动保证业务多步操作的事务性。如果需要把多个操作作为一个不可分割的整体,外部锁或更高层的并发控制仍然必要。
为什么 containsKey 再 put 不是线程安全的?
因为两个方法调用之间存在时间窗口。线程 A 判断 key 不存在后,线程 B 可能已经插入同一个 key,线程 A 再 put 就会覆盖或造成重复初始化。应使用 putIfAbsent 或 computeIfAbsent 让检查和插入成为一个原子操作。
ConcurrentHashMap 的 get 需要加锁吗?
通常不需要。它的实现通过 volatile、CAS、局部同步等机制保证并发可见性和结构安全,读操作大多可以无锁完成。但这只保证容器层面的读取安全,不代表读到的 value 对象内部状态也一定线程安全。
ConcurrentHashMap 遍历时能看到最新数据吗?
不保证。它提供弱一致遍历,可能看到遍历开始前或遍历过程中一部分更新,也可能看不到部分并发更新。它的优势是遍历不会因为并发修改而失败,适合对精确快照要求不高的场景。
读多写少为什么适合不可变快照?
不可变快照让读线程始终访问一个稳定 Map,不需要加锁,也不会读到被修改到一半的结构。写线程构造新 Map 后一次性替换引用,发布新版本。代价是每次写入可能复制较多数据,并产生额外内存压力。
HashTable 和 ConcurrentHashMap 有什么区别?
Hashtable 基本是整表方法级同步,所有读写竞争同一把锁,并发性能差。ConcurrentHashMap 使用更细粒度的并发控制,读写冲突更少,适合高并发场景。