「Java」HashMap底层实现、加载因子、容量值及死循环

开发 后端
HashMap是一个基于哈希表实现的无序的key-value容器,它键和值允许设置为 null,同时它是线程不安全的。

HashMap 简介

HashMap是一个基于哈希表实现的无序的key-value容器,它键和值允许设置为 null,同时它是线程不安全的。

HashMap 底层实现

  •  在jdk 1.7中HashMap是以数组+链表的实现的
  •  在jdk1.8开始引入红黑树,HashMap底层变成了数组+链表+红黑树实现

红黑树简介

红黑树是一种特殊的平衡二叉树,它有如下的特征:

  •  节点是红色或黑色
  •  根节点是黑色的
  •  所有叶子都是黑色。(叶子是NULL节点)
  •  每个红色节点的两个子节点都是黑色的(从每个叶子到根的所有路径上不能有两个连续的红色节点)
  •  从任一节点到其每个叶子的所有路径都包含相同数目的黑色节点。

所以红黑树的时间复杂度为: O(lgn)。

jdk1.8:数组+链表+红黑树

HashMap的底层首先是一个数组,元素存放的数组索引值就是由该元素的哈希值(key-value中key的哈希值)确定的,这就可能产生一种特殊情况——不同的key哈希值相同。

在这样的情况下,于是引入链表,如果key的哈希值相同,在数组的该索引中存放一个链表,这个链表就包含了所有key的哈希值相同的value值,这就解决了哈希冲突的问题。

但是如果发生大量哈希值相同的特殊情况,导致链表很长,就会严重影响HashMap的性能,因为链表的查询效率需要遍历所有节点。于是在jdk1.8引入了红黑树,当链表的长度大于8,且HashMap的容量大于64的时候,就会将链表转化为红黑树。 

  1. // jdk1.8  
  2. // HashMap#putVal  
  3. // binCount 是该链表的长度计数器,当链表长度大于等于8时,执行树化方法  
  4. // TREEIFY_THRESHOLD = 8  
  5. if (binCount >= TREEIFY_THRESHOLD - 1)  
  6.     treeifyBin(tab, hash);  
  7. // HashMap#treeifyBin      
  8. final void treeifyBin(Node<K,V>[] tab, int hash) {  
  9.     int n, index; Node<K,V> e;  
  10.     // MIN_TREEIFY_CAPACITY=64  
  11.     // 若 HashMap 的大小小于64,仅扩容,不树化  
  12.     if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY 
  13.         resize();  
  14.     else if ((e = tab[index = (n - 1) & hash]) != null) {  
  15.         TreeNode<K,V> hd = nulltl = null 
  16.         do { 
  17.              TreeNode<K,V> p = replacementTreeNode(e, null);  
  18.             if (tl == null)  
  19.                 hd = p
  20.              else {  
  21.                 p.prev = tl
  22.                  tl.next = p 
  23.             }  
  24.             tl = p 
  25.         } while ((ee = e.next) != null);  
  26.         if ((tab[index] = hd) != null)  
  27.             hd.treeify(tab);  
  28.     }  

加载因子为什么是0.75

所谓的加载因子,也叫扩容因子或者负载因子,它是用来进行扩容判断的。

假设加载因子是0.5,HashMap初始化容量是16,当HashMap中有16 * 0.5=8个元素时,HashMap就会进行扩容操作。

而HashMap中加载因子为0.75,是考虑到了性能和容量的平衡。

由加载因子的定义,可以知道它的取值范围是(0, 1]。

  •  如果加载因子过小,那么扩容门槛低,扩容频繁,这虽然能使元素存储得更稀疏,有效避免了哈希冲突发生,同时操作性能较高,但是会占用更多的空间。
  •  如果加载因子过大,那么扩容门槛高,扩容不频繁,虽然占用的空间降低了,但是这会导致元素存储密集,发生哈希冲突的概率大大提高,从而导致存储元素的数据结构更加复杂(用于解决哈希冲突),最终导致操作性能降低。
  •  还有一个因素是为了提升扩容效率。因为HashMap的容量(size属性,构造函数中的initialCapacity变量)有一个要求:它一定是2的幂。所以加载因子选择了0.75就可以保证它与容量的乘积为整数。 
  1. // 构造函数  
  2. public HashMap(int initialCapacity, float loadFactor) {  
  3.     // ……  
  4.     this.loadFactor = loadFactor;// 加载因子  
  5.     this.threshold = tableSizeFor(initialCapacity);  
  6.  
  7. /**  
  8.  * Returns a power of two size for the given target capacity.返回2的幂  
  9.  * MAXIMUM_CAPACITY = 1 << 30  
  10.  */  
  11. static final int tableSizeFor(int cap) {  
  12.     int n = cap - 1;  
  13.     n |= n >>> 1;  
  14.     n |= n >>> 2; 
  15.     n |= n >>> 4;  
  16.     n |= n >>> 8;  
  17.     n |= n >>> 16;  
  18.     return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;  

HashMap 的容量为什么是2的 n 次幂

HashMap的默认初始容量是16,而每次扩容是扩容为原来的2倍。这里的16和2倍就保证了HashMap的容量是2的n次幂,那么这样设计的原因是什么呢?

原因一:与运算高效

与运算&,基于二进制数值,同时为1结果为1,否则就是0。如1&1=1,1&0=0,0&0=0。使用与运算的原因就是对于计算机来说,与运算十分高效。

原因二:有利于元素充分散列,减少 Hash 碰撞

在给HashMap添加元素的putVal函数中,有这样一段代码: 

  1. // n为容量,hash为该元素的hash值  
  2. if ((p = tab[i = (n - 1) & hash]) == null)  
  3.     tab[i] = newNode(hash, key, value, null); 

它会在添加元素时,通过i = (n - 1) & hash计算该元素在HashMap中的位置。

当 HashMap 的容量为 2 的 n 次幂时,他的二进制值是100000……(n个0),所以n-1的值就是011111……(n个1),这样的话(n - 1) & hash的值才能够充分散列。

举个例子,假设容量为16,现在有哈希值为1111,1110,1011,1001四种将被添加,它们与n-1(15的二进制=01111)的哈希值分别为1111、1110、1110、1011,都不相同。

而假设容量不为2的n次幂,假设为10,那么它与上述四个哈希值进行与运算的结果分别是:0101、0100、0001、0001。

可以看到后两个值发生了碰撞,从中可以看出,非2的n次幂会加大哈希碰撞的概率。所以 HashMap 的容量设置为2的n次幂有利于元素的充分散列。

参考:HashMap初始容量为什么是2的n次幂及扩容为什么是2倍的形式

HashMap 是如何导致死循环的

HashMap会导致死循环是在jdk1.7中,由于扩容时的操作是使用头插法,在多线程的环境下可能产生循环链表,由此导致了死循环。在jdk1.8中改为使用尾插法,避免了该死循环的情况。 

 

责任编辑:庞桂玉 来源: segmentfault
相关推荐

2013-06-06 13:34:56

HashMap线程不安全

2020-12-17 07:39:30

HashMap死循环数据

2023-01-04 07:54:03

HashMap底层JDK

2024-10-10 10:09:56

HashMap死循环数据损坏

2020-09-29 15:24:07

面试数据结构Hashmap

2022-01-20 08:44:25

HashMap死循环开放性

2022-01-18 06:59:50

HashMap循环底层

2023-07-11 08:00:00

2023-01-31 08:24:55

HashMap死循环

2022-01-13 06:59:40

HashMap底层面试

2011-08-29 16:23:29

Lua脚本

2021-08-29 07:41:48

数据HashMap底层

2015-10-09 09:43:53

云环境CPU虚拟化底层容量

2020-08-19 16:36:53

HashMap红黑树阈值

2023-10-18 10:55:55

HashMap

2018-10-10 20:20:14

2011-09-07 10:13:04

IPv6IPv4

2024-10-30 11:30:02

2013-06-06 13:10:44

HashMap无锁

2022-01-24 07:01:20

安全多线程版本
点赞
收藏

51CTO技术栈公众号