集合框架终结篇:HashMap、Hashtable、ConcurrentHashMap 到底怎么选
三篇聊下来HashMap 的核心机制、进阶原理、并发安全都过了一遍了。这篇是系列最后一篇聊聊一个我在面试里被反复问到的问题HashMap、Hashtable、ConcurrentHashMap 到底有什么区别分别应该在什么时候用这个问题表面上是问三个类的区别实际上是在考察你对整个哈希表体系的理解。如果你能把前几篇的内容串起来这个问题其实不难回答。但有个前提你得先搞清楚 Hashtable 是什么。Hashtable 是什么说实话我第一次看到 Hashtable 这个名字的时候第一反应是这不就是 HashMap 加了个 s 吗后来才发现这两个东西除了名字像差别还挺大的。Hashtable 是 Java 早期就有的一个类JDK 1.0 就有了。那个时候还没有集合框架Java 集合框架是 JDK 1.2 才引入的所以它跟 Vector 一样算是那个时代的产物。它的底层结构其实跟 JDK 1.7 之前的 HashMap 差不多数组加链表。数组是主体链表用来解决哈希冲突。JDK 1.8 的 HashMap 加了红黑树但 Hashtable 一直没加不是不能加是它基本已经被淘汰了没必要继续维护。它的线程安全实现方式也很原始每个方法都加 synchronized。publicsynchronizedVput(Kkey,Vvalue){...}publicsynchronizedVget(Objectkey){...}这意味着什么同一时刻只有一个线程能调用 Hashtable 的方法。不管你是在读还是在写不管操作的是哪个 key都得排队。我以前写过一个比喻Hashtable 就像只有一个收银台的超市所有顾客排一条队。ConcurrentHashMap 像有多个收银台的大超市顾客可以分流到不同的收银台。HashMap 则像无人超市不用排队但也没人管秩序可能会出乱子。Hashtable 的一些特性有几个点值得一提。第一Hashtable 不支持 null key 和 null value。如果你传一个 null 进去会直接抛 NullPointerException。原因跟 ConcurrentHashMap 一样多线程环境下null 返回值无法区分key 不存在和key 对应的 value 就是 null。第二Hashtable 的扩容策略跟 HashMap 不一样。HashMap 默认容量是 16扩容翻倍变成 2 的 n 次方。Hashtable 默认容量是 11扩容变成 2n1原来的 2 倍加 1。而且 Hashtable 的索引计算用的是取模hash % tab.length不是 HashMap 的位运算hash (length - 1)。因为它的容量不是 2 的 n 次方用不了位运算优化。第三Hashtable 底层只有数组加链表没有红黑树。所以如果哈希冲突严重某个桶的链表变得很长查找效率就会从 O(1) 掉到 O(n)。而 HashMap 在这一点上做了优化链表太长就转红黑树。HashMap 和 Hashtable 的区别这个问题面试出现的频率挺高的我当初准备面试的时候专门背过一张对比表。我把它整理了一下维度HashMapHashtable线程安全否是全方法 synchronizednull key/value支持不支持默认容量1611扩容策略翻倍2 的幂次方2n1索引计算hash (length - 1)hash % length底层结构数组链表红黑树JDK 1.8数组链表继承AbstractMapDictionary过时的抽象类性能高低现状广泛使用基本被淘汰有几个点我觉得值得展开说一下。线程安全这部分要注意Hashtable 虽然是线程安全的但它的安全是以牺牲性能为代价的。每个方法都加 synchronized意味着任何时刻只能有一个线程操作。在低并发场景下可能还能用一旦并发上去性能瓶颈就很明显了。null key 的处理差异HashMap 明确支持 null key会在 put 的时候特殊处理把 hash 值设为 0放入 0 号桶。Hashtable 则直接拒绝因为它的 put 方法里会先检查 value 是否为 nullpublicsynchronizedVput(Kkey,Vvalue){if(valuenull){thrownewNullPointerException();}// ...}注意这里只检查了 value没检查 key。但 key 是通过key.hashCode()来算索引的key 为 null 的话调 hashCode 会抛 NPE。扩容差异Hashtable 用 2n1 而不是 2 倍目的其实是为了让容量是奇数或者说是质数取模运算时分布更均匀。但 HashMap 选择了另一种思路用位运算代替取模通过让容量是 2 的幂次方来实现快速寻址。两种思路各有道理但从实际效果来看HashMap 的方案更优因为位运算比取模快太多了。现状Hashtable 基本已经被淘汰了。我学 Java 到现在没有在任何一个实际项目里见到过 Hashtable 的使用。它更像是一个历史遗迹告诉后来的开发者我们曾经是这样做的现在有更好的方式了。知道它的存在、能说出它的问题就够了不用花太多精力去研究它。ConcurrentHashMap 比 Hashtable 好在哪里这个问题其实在上一篇已经详细讲过了这里只做对比总结。Hashtable 和 ConcurrentHashMap 都是线程安全的 Map但设计思路完全不同。Hashtable 的思路是不管你是谁进来先排队。所有方法都加 synchronized简单粗暴但并发度是 1。ConcurrentHashMap 的思路是如果你们不冲突就不用排队。JDK 1.7 用分段锁把数据分成多个 Segment不同 Segment 之间的操作可以并发。JDK 1.8 更进一步把锁粒度缩小到每个桶的头节点配合 CAS 实现更细粒度的并发控制。我把三个放到一起对比HashtableConcurrentHashMap 1.7ConcurrentHashMap 1.8锁粒度整个对象每个 Segment每个桶头节点锁类型synchronizedReentrantLockCAS synchronized并发度116默认 Segment 数数组长度底层结构数组链表SegmentHashEntry数组链表/红黑树null key/value不支持不支持不支持ConcurrentHashMap 和 Hashtable 都不支持 null key/value。原因是一样的多线程环境下get 返回 null 的时候你没法区分是这个 key 不存在还是这个 key 对应的 value 就是 null。HashMap 单线程用的时候没这个问题因为你可以通过 containsKey 来判断。但多线程环境下你调 containsKey 和 get 之间可能有其他线程修改了数据所以这种判断就不靠谱了。干脆直接不支持 null反而更安全。三种 Map 怎么选学完这么多理论最后还是要落到实际选择上。我自己的经验总结成一句话单线程用 HashMap多线程用 ConcurrentHashMap基本不会用到 Hashtable。展开说说场景一单线程环境。唯一的选择就是 HashMap。简单、高效没有额外的并发开销。绝大多数业务场景都是单线程处理请求所以 HashMap 的使用频率最高。场景二多线程环境读写操作都比较频繁。用 ConcurrentHashMap。这是最通用的并发 Map 方案。它的分段锁 / CASsynchronized 机制保证了高并发下的性能。不管是读多写少还是读写均衡ConcurrentHashMap 都能应对。场景三多线程环境需要排序。用 ConcurrentSkipListMap。它基于跳表实现能保持 key 的有序性。ConcurrentHashMap 不保证顺序如果业务上需要按 key 的顺序遍历就得用这个。场景四多线程环境但只是偶尔读一下几乎没有写。如果你百分之百确定没有写操作用 Collections.unmodifiableMap 包装一个 HashMap 就行。但如果不确定还是用 ConcurrentHashMap 更安全。Hashtable 在什么时候用说实话我想不出什么场景会主动选择 Hashtable。如果你需要一个线程安全的 MapConcurrentHashMap 在任何维度上都优于 Hashtable。Hashtable 的存在更多是历史原因它在 ConcurrentHashMap 出现之前就被广泛使用了但因为性能问题现在基本没人会在新代码里用它。面试的时候可能会问它的区别但实际写代码的时候我的建议是忘了 Hashtable 吧记住 ConcurrentHashMap 就够了。把三条线串起来三篇 HashMap 相关的文章写下来从核心机制到进阶原理到并发安全再到这一篇的三者对比我觉得这个链条算是完整了。回想一下我学这些东西的历程。一开始我只知道 HashMap 存键值对的用起来很方便。后来发现多线程下会出问题知道了 ConcurrentHashMap 这个存在。再后来面试被问到为什么 HashMap 的容量是 2 的 n 次方“为什么用红黑树”“分段锁怎么工作的”才开始去翻源码。源码看了一遍又一遍有些地方看了好几遍才看懂。看懂了之后又去写 demo 验证确认自己的理解是对的。这个过程挺痛苦的但也是最有收获的。那些背下来的面试题在翻了源码之后才真正变成了自己的理解。现在回过头来看这四篇文章其实就是我自己的学习笔记。从一个新手的视角把 HashMap 从底层到顶层串了一遍。如果你也是刚学这些东西希望这个系列能帮你少走一些弯路。不用担心一开始记不住那么多细节先把整体框架搭起来用到的时候再回去查。写代码多了自然就熟练了。毕竟我也是这么一点点啃过来的。Hashtable 与集合对比总结Map 三雄特性横向对比定义与边界这块讲 Hashtable 的底层实现原理以及 HashMap、Hashtable、ConcurrentHashMap 三者的全面对比。包括 Hashtable 的数据结构、线程安全实现方式、三种 Map 在底层数据结构、线程安全机制、null 键值支持、初始化容量、扩容策略、性能特点等方面的差异。不包括 HashMap 和 ConcurrentHashMap 的详细内部原理分别归入对应分类。内容归属Hashtable 底层实现原理底层数据结构数组 链表数组是主体链表是解决哈希冲突存在的。Hashtable 的所有公共方法均采用 synchronized 关键字。当一个线程访问同步方法时其他线程访问就会陷入阻塞或轮询状态。不支持 null 键和值put(null, value)会抛 NullPointerException。默认初始容量为 11每次扩容变为原来的 2n1。创建时给定了初始容量会直接用给定的大小不做 2 的幂次方调整。Hashtable 线程安全实现Hashtable 的 put 和 get 都做成同步方法publicsynchronizedVput(Kkey,Vvalue){...}publicsynchronizedVget(Objectkey){...}这种实现方式导致任何一个时刻只能有一个线程操作 Hashtable效率比较低。HashMap 与 Hashtable 区别维度HashMapHashtable线程安全否是synchronizednull key/value支持 null key 和 null value不支持 null key/value默认初始容量1611扩容策略2 倍2 的幂次方2n1底层结构数组链表红黑树JDK 1.8数组链表效率高低全方法同步现状广泛使用基本被淘汰Hashtable 与 ConcurrentHashMap 区别维度HashtableConcurrentHashMap底层结构数组链表JDK 1.7分段数组链表JDK 1.8数组链表/红黑树线程安全方式synchronized 锁整个对象JDK 1.7分段锁SegmentReentrantLockJDK 1.8CASsynchronized 锁桶头节点并发度低一次只能一个线程高多线程可并发操作不同段/桶null key/value不支持不支持多线程下无法区分key 不存在和value 就是 null性能低高HashMap、Hashtable、ConcurrentHashMap 对比总结HashMap线程不安全效率高支持 null key/value。默认初始容量 16扩容 2 倍保持 2 的幂次方。底层数组链表红黑树JDK 1.8。Hashtable线程安全全方法 synchronized效率低不支持 null key/value。默认初始容量 11扩容 2n1。底层数组链表基本被淘汰。ConcurrentHashMap线程安全高性能不支持 null key/value抛 NPE。JDK 1.7分段锁Segment 数组每个 Segment 拥有自己的数组和独立的 ReentrantLock读写可并行。JDK 1.8取消 Segment直接锁桶头节点数组链表/红黑树volatileCASsynchronized锁粒度从段缩小到桶并发度更高。与其他分类的关系Hashtable 不依赖任何其他分类即可独立阅读。HashMap 的详细原理请见HashMap 核心机制和HashMap 进阶设计原理ConcurrentHashMap 的详细原理请见并发安全与 ConcurrentHashMap。