5. HashMap 的性能参数:
HashMap 包含如下几个构造器:
HashMap() :构建一个初始容量为 16 ,负载因子为 0.75 的 HashMap 。
HashMap(int initialCapacity) :构建一个初始容量为 initialCapacity ,负载因子为 0.75 的 HashMap 。
HashMap(int initialCapacity, float loadFactor) :以指定初始容量、指定的负载因子创建一个 HashMap 。
HashMap 的基础构造器 HashMap(int initialCapacity, float loadFactor) 带有两个参数,它们是初始容量 initialCapacity 和加载因子 loadFactor 。
initialCapacity : HashMap 的最大容量,即为底层数组的长度。
loadFactor :负载因子 loadFactor 定义为:散列表的实际元素数目 (n)/ 散列表的容量 (m) 。
负载因子衡量的是一个散列表的空间的使用程度,负载因子越大表示散列表的装填程度越高,反之愈小。对于使用链表法的散列表来说,查找一个元素的平均时间是 O(1+a) ,因此如果负载因子越大,对空间的利用更充分,然而后果是查找效率的降低;如果负载因子太小,那么散列表的数据将过于稀疏,对空间造成严重浪费。
HashMap 的实现中,通过 threshold 字段来判断 HashMap 的最大容量:
threshold = (int)(capacity * loadFactor);
结合负载因子的定义公式可知, threshold 就是在此 loadFactor 和 capacity 对应下允许的最大元素数目,超过这个数目就重新 resize ,以降低实际的负载因子。默认的的负载因子 0.75 是对空间和时间效率的一个平衡选择。当容量超出此最大容量时, resize 后的 HashMap 容量是容量的两倍:
if (size++ >= threshold)
resize(2 * table.length);
6. Fail-Fast 机制:
我们知道 java.util.HashMap 不是线程安全的,因此如果在使用迭代器的过程中有其他线程修改了 map ,那么将抛出 ConcurrentModificationException ,这就是所谓 fail-fast 策略。
这一策略在源码中的实现是通过 modCount 域, modCount 顾名思义就是修改次数,对 HashMap 内容的修改都将增加这个值,那么在迭代器初始化过程中会将这个值赋给迭代器的 expectedModCount 。
HashIterator() {
expectedModCount = modCount;
if (size > 0) { // advance to first entry
Entry[] t = table;
while (index < t.length && (next = t[index++]) == null)
;
}
}
在迭代过程中,判断 modCount 跟 expectedModCount 是否相等,如果不相等就表示已经有其他线程修改了 Map:
注意到 modCount 声明为 volatile ,保证线程之间修改的可见性。
final Entry<K,V> nextEntry() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
在 HashMap 的 API 中指出:
由所有 HashMap 类的 “collection 视图方法 ” 所返回的迭代器都是快速失败的:在迭代器创建之后,如果从结构上对映射进行修改,除非通过迭代器本身的 remove 方法,其他任何时间任何方式的修改,迭代器都将抛出 ConcurrentModificationException 。因此,面对并发的修改,迭代器很快就会完全失败,而不冒在将来不确定的时间发生任意不确定行为的风险。
注意,迭代器的快速失败行为不能得到保证,一般来说,存在非同步的并发修改时,不可能作出任何坚决的保证。快速失败迭代器尽最大努力抛出 ConcurrentModificationException 。因此,编写依赖于此异常的程序的做法是错误的,正确做法是:迭代器的快速失败行为应该仅用于检测程序错误。
参考资料:
JDK API HashMap HashMap 源代码 深入理解 HashMap
通过分析 JDK 源代码研究 Hash 存储机制 java.util.HashMap源码要点浅析