题目:HashMap、Hashtable 和 ConcurrentHashMap 的区别?
难度:low
标签:java/collections, general/knowledge
评分:⭐⭐⭐☆☆(3/5)
Hashmap,Hashtable,ConcurrentHashMap 三者核心底层都是 HashMap,都是用来存储 key value 类型的内容。最大的差异其实是对于并发的支持。HashMap 本身不支持并发,多线程并发,它是线程多线程下不安全的。而 Hashtable 跟 ConcurrentHashMap 都是多线程下线程安全的,但是它们两个的机制不一样,Hashtable 是在整体上整个方法上就添加了 synchronized 锁。所冲突的,在进行对应的数组上添加数据的时候才会添加锁。也就是说 ConcurrentHashMap 的加锁力度要比 Hashtable 的要少。目前 JDK 已经不推荐使用 Hashtable,因为它加锁的力度太大,效率太低了。常规正常情况下还是使用 HashMap,然后只有在存在并发的场景使用 ConcurrentHashMap。
优点:
Hashtable 方法级 synchronized vs ConcurrentHashMap 桶级锁Hashtable不足:
HashMap 允许一个 null key + 多个 null value;Hashtable 和 ConcurrentHashMap 都不允许(空值会 NPE)HashMap 16,Hashtable 11HashMap/Hashtable 是 fail-fast;ConcurrentHashMap 是 weakly-consistent(弱一致,遍历中修改不抛异常)——这是它并发友好的核心之一synchronized + CASHashtable 从 1.0 就有,JDK5 后被 Collections.synchronizedMap(HashMap) 和 ConcurrentHashMap 双重替代Hashtable 只有链表;JDK8 HashMap 有链表 + 红黑树补充:
Hashtable 淘汰原因:整表锁导致读操作也串行化,吞吐瓶颈;没有 fail-fast 语义(弱一致迭代,可能读到脏数据)ConcurrentHashMap 演进:JDK1.7 分段锁(16 个 Segment,每个 Segment 一把 ReentrantLock)→ JDK1.8 桶级 synchronized + CAS 写入 + sizeCtl 协助扩容,写冲突粒度进一步细化到桶头ConcurrentHashMap 迭代器基于快照,允许边遍历边修改,但可能读到修改前/后的值——适合读多写少的高并发统计场景核心考点定位:验证是否掌握三种 Map 的线程安全模型差异、JDK 演进、以及并发场景的正确选型。
知识点拆解:
HashMap 单线程裸奔,Hashtable 全表锁 + 拒绝 null,ConcurrentHashMap 分段→桶级细粒度锁 + 允许更高并发。Hashtable 把每个方法都 synchronized,锁的是 this(整个表),任何读/写都串行化;ConcurrentHashMap JDK7 用 16 个 Segment,每个 Segment 是独立的锁对象,JDK8 进一步细化到桶头节点,读操作几乎无锁(volatile 读 + CAS),只有同桶写入才竞争。HashMap;单线程转并发优先 ConcurrentHashMap;只需要"包一层同步"的低并发场景可用 Collections.synchronizedMap;Hashtable 只在对接遗留 API 时保留。HashMap fail-fast(ConcurrentModificationException);Hashtable 事实上弱一致但被官方标注为不可靠;ConcurrentHashMap 弱一致迭代器是设计保证,不抛 CME。按「底层原理 → 设计权衡 → 工程实践」递进排列。
考察方向:
ConcurrentHashMap 从 1.7 分段锁到 1.8 桶级 synchronized + CAS 的演进原因ConcurrentLinkedQueue、CopyOnWriteArrayList、Collections.synchronizedMap 等并发容器形成选型谱系延伸递进面试题:
ConcurrentHashMap JDK7 和 JDK8 锁结构有什么变化?为什么这样改?synchronized。JDK8 取消 Segment 的原因:写多场景 Segment 仍是瓶颈,桶级锁更细;配合 CAS 处理桶为空场景,扩容时协助迁移。ConcurrentHashMap 如何保证 size() 是准的?baseCount + counterCells[](类似 LongAdder)分片计数,size() 遍历累加,返回值可能是"某个瞬间的近似值",不保证强一致。Collections.synchronizedMap,什么时候用 ConcurrentHashMap?synchronizedMap;高并发读多写少/需要迭代时选 ConcurrentHashMap。前者需要自己处理迭代器外层加锁,后者不需要。HashMap 在 JDK8 引入红黑树但 Hashtable 一直没有?Hashtable 是 JDK1.0 遗留物,早期未维护,JDK5 后官方推荐迁移到 ConcurrentHashMap 或 Collections.synchronizedMap;继续维护投入产出比低。面试官您好,这三个 Map 底层都是哈希表结构,最大差异在并发模型和空值策略。
HashMap 单线程裸奔,不保证线程安全,但性能最好;允许 1 个 null key 和多个 null value,默认容量 16,JDK8 里当桶内链表长度超过 8 且整个表容量超过 64 时会转成红黑树。
Hashtable 是 JDK1.0 的遗留物,把每个公共方法都加了 synchronized,锁的是整张表,读也串行化;同时它不接受 null key 或 null value,会抛 NPE。JDK5 之后官方就不推荐使用它了。
ConcurrentHashMap 是并发场景的默认选择。它的演进很有意思:JDK1.7 用 16 个 Segment 分段锁,每个 Segment 内部还有一张小 HashMap,一次加锁只锁一个 Segment;JDK1.8 进一步去掉 Segment,改用桶头节点的 synchronized 加 CAS——空桶写入用 CAS,非空桶写入锁桶头,扩容时用 sizeCtl 协助多线程迁移节点。这样写冲突的粒度细化到桶级,读操作几乎无锁。
迭代器也有差异:HashMap 和 Hashtable 都是 fail-fast,遍历时被修改会抛 ConcurrentModificationException;ConcurrentHashMap 是弱一致迭代器,遍历时允许修改,不抛异常,但可能读到修改前后的任意快照——这在读多写少的高并发统计场景非常合适。
选型上,单线程用 HashMap;只在极少数低并发场景需要线程安全时可以用 Collections.synchronizedMap 包一层;一旦进入真正并发场景,直接用 ConcurrentHashMap;Hashtable 只在对接历史 API 时才见到。