解读 Java 8 HashMap

前言

这篇文章是记录本身分析 Java 8 的 HashMap 源码时遇到的疑问和总结,在分析的过程当中笔者把遇到的问题都记录下来,而后逐一击破,若是有错误的地方,但愿读者能够指正,笔者感激涕零。java

疑问与解答

什么是 initial capacity?

The capacity is the number of buckets in the hash table, and the initial capacity is simply the capacity at the time the hash table is created.git

initial capacity 指定了 HashMap 内部的 hash table 的初始化容量,能够经过构造函数指定,默认的初始化容量为 16github

static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
复制代码

什么是 load factor?

The load factor is a measure of how full the hash table is allowed to get before its capacity is automatically increased. When the number of entries in the hash table exceeds the product of the load factor and the current capacity, the hash table is rehashed (that is, internal data structures are rebuilt) so that the hash table has approximately twice the number of buckets.数组

能够看到 load factor 是用于肯定 hash table 什么时候扩容的重要参数之一。安全

为何 initial capacity 过高或 load factor 过低会影响性能?

Iteration over collection views requires time proportional to the "capacity" of the HashMap instance (the number of buckets) plus its size (the number of key-value mappings). Thus, it's very important not to set the initial capacity too high (or the load factor too low) if iteration performance is important.数据结构

An instance of HashMap has two parameters that affect its performance: initial capacity and load factor. The capacity is the number of buckets in the hash table, and the initial capacity is simply the capacity at the time the hash table is created. The load factor is a measure of how full the hash table is allowed to get before its capacity is automatically increased. When the number of entries in the hash table exceeds the product of the load factor and the current capacity, the hash table is rehashed (that is, internal data structures are rebuilt) so that the hash table has approximately twice the number of buckets. app

查看源码描述函数

描述主要提到三个问题:性能

  1. initial capacity 和 load factor 是重要的性能参数。
  2. 设置不当会影响迭代性能(若是迭代性能很重要的话)
  3. 设置不当会致使频繁的 rehash(伴随 hash table 的 rebuild,而且 hash table 的容量会成倍增长)

合理的 initial capacity 是多少?

笔者通过一番测试,在 hash 均匀分布的前提下,数据量从 100 ~ 千万级,initial capacity 设置从 1 到千万不等,发现这个值的大小对迭代性能和 rehash 的影响不是很大(到千万级别才会有稍微明显的效率问题),缘由是扩容时,每次都会 double threshold,有效避免了高频率的扩容动做;对迭代性能的影响也很是低(10W数据,1亿初始容量,毫秒级别影响)。测试

合理的 load factor 是多少?

默认 0.75 便可,过低可能会致使频繁的扩容,而且有可能致使 java.lang.OutOfMemoryError: Java heap space

何时会发生 resize?

When 
putting data && (hashtable.length == 0 || entries.length >= threshold)

Then resize() 复制代码

何时会发生 rehashed?

触发 resize 的时候,会发生一次扩容,并随 rehash。

为何须要 rehashed?

扩容时会重建 hash table,因为新 hash table 的容量 = 旧 hash table 容量左移一位,所以须要 rehash,以确保新的 hash 落在 [0, new_capacity) 区间内。

如何确保 hash 落入 [0, capacity) 区间?

(capacity - 1) & (hashcode ^ (hashcode >>> 16))

capacity 取值范围: 2^(N+) - 1,0 < (N+) < 31。

在 hashCode 均匀分布的前提下(Object.hashCode() 默认为每一个对象生成不一样的 hash code,具体原理能够看 java doc),hashcode ^ (hashcode >>> 16) 能够下降 hash 冲突的概率(相对于 (capacity - 1) & hashcode),原理是混合原始哈希码的高位和低位,以此来加大低位的随机性;(capacity - 1) & new_hash 能够保证计算出来的 index 落入 [0, capacity)。

为何扩容后是原来的 2 倍,而不是 1.5 倍?3 倍?4 倍?

  1. 扩容原来的 2 倍可让 rehash 的值更加稳定,总体的 hash 值均匀分散。
  2. 减小没必要要的扩容空间(影响迭代性能)。

如何模拟 hash 碰撞?

经过覆写 Object##hashCode()方法便可自定义 hashCode,就能够模拟 hash 碰撞,例如随机 hashCode 或固定 hashCode。

何时 HashMap 会采用红黑树保存节点数据?

Given
TREEIFY_THRESHOLD = 8

When
hashCount >= TREEIFY_THRESHOLD Then treeifyBin(bin) 复制代码

当出现同一个 hash 达到 8 次碰撞,就会从链表转换成红黑树。

什么是 hash table

hash table 本质上是一个数组 + 链表或红黑树的数据结构, hash table 经过创建 hash 到数据节点的映射关系,巧妙的达成 O(1) 的检索效率。其中每一个链表保存着 hash 冲突(key 不相等,hash 值相等)的数据项,默认状况下,当达到 8 个冲突时,链表就会转换成红黑树,转换以后还伴随着 O(n) -> O(log n) 的效率提高,不过这种状况出现的几率很低(在分散的 hash code 的前提下,相同的 hash 值产生 8 次冲突的几率仅为千万分之 6),不过能够经过人为模拟重现这种状况。

什么状况下,HashMap 的效率最差?

频繁的 hash 冲突是致使 HashMap 性能降低的罪魁祸首,当同一个 hash 值冲突达到 8 次及以上时, 此时 rbtree 可能须要常常经过左旋、右旋和着色来保持自身平衡,这个代价之大跟同一个 hash 值的冲突次数成正比,所以须要维护好重写的 hashCode 方法,使 hash code 尽量分散。

为何采用 RBTree 替代 LinkedList 来保存 hash 冲突的的节点?

用于提升哈希冲突条件下的性能,同时去除了之前版本遗留下来的问题,具体能够看这里 openjdk.java.net/jeps/180

什么是红黑树?

红黑树(英语:Red–black tree)是一种自平衡二叉查找树,是在计算机科学中用到的一种数据结构,典型的用途是实现关联数组。它在1972年由鲁道夫·贝尔发明,被称为"对称二叉B树",它现代的名字源于Leo J. Guibas和Robert Sedgewick于1978年写的一篇论文。红黑树的结构复杂,但它的操做有着良好的最坏状况运行时间,而且在实践中高效:它能够在 O(log n)时间内完成查找,插入和删除,这里的 n 是树中元素的数目。

上面是维基百科中的定义,同时红黑树还有 5 个性质,红黑树是每一个节点都带有颜色属性的二叉查找树,颜色为红色或黑色。在二叉查找树强制通常要求之外,对于任何有效的红黑树增长了以下的额外要求:

  1. 节点是红色或黑色。
  2. 根是黑色。
  3. 全部叶子都是黑色(叶子是NIL节点)。
  4. 每一个红色节点必须有两个黑色的子节点。(从每一个叶子到根的全部路径上不能有两个连续的红色节点。)
  5. 从任一节点到其每一个叶子的全部简单路径都包含相同数目的黑色节点。

红黑树适合哪些应用场景?

时间敏感型程序。

红黑树有哪些应用场景?

Linux 内核和 epoll 系统调用实现中使用的彻底公平调度程序使用红黑树。

什么是 fail-fast?

HashMap 经过这个机制确保迭代时,若是修改 map 的结构(增,删),一经发现则当即抛出 ConcurrentModificationException,而不是等到将来的某个时刻再通知异常,能够经过这个机制来捕获程序的一些 BUG.

能够经过什么方式使 HashMap 线程安全.

在 HashMap 之上包装多一层,而且使用 synchronized 同步锁锁定 HashMap 实例的全部公开接口,Collections#synchronizedMap 已经提供了这样一种实现。

总结

经过阅读源码、查阅资料和动手验证,才知道 HashMap 的知识点那么多,不过都啃得差很少以后,就会以为知识点不多(应该还有遗漏掉的或者不严谨的状况),确实让本身学到了不少以前不知道的知识点。

在接下来的文章中,笔者会经过 TDD (测试驱动开发)来记录如何实现一个精简版的 HashMap,避免一看就会,一作就废的尴尬局面。

相关文章
相关标签/搜索