HashMap学习
简单介绍
HashMap是基于一个散列表实现(设计用来替代HashTable)。针对键-值的插入和检索,保证了它稳定的性能。
我们可以把先HashMap可以看做是一个存储了很多个键值对的数组,每一个键值对我们把它叫做一个Entry,这些Entry通过一定的规则分散的存储在数组里面,HashMap数组每一个元素的初始值都是Null。
-
Put方法的原理
比如调用 hashMap.put("apple", 0) ,插入一个Key为“apple"的元素。这时候我们需要确定Entry的插入位置
首先对key进行hash运算
再由hash和lenth进行运算
index=(lenth - 1) & hash
假定最后计算出的index是2,那么结果如下:
但是,因为HashMap的长度是有限的,当插入的Entry越来越多时,再完美的Hash函数也难免会出现index冲突的情况。比如下面这样:
这时候该怎么办呢?我们可以利用链表来解决。
HashMap采用Entry数组来存储key-value对,每一个键值对组成了一个Entry实体,Entry类实际上是一个单向的链表结构,它具有Next指针,可以连接下一个Entry实体,以此来解决Hash冲突的问题。
需要注意的是,新来的Entry节点插入链表时,jdk8之前使用的是“头插法”。jdk8之后就是使用“尾插法”了,主要为了安全,避免链条成环,这里不详细讲解了
在JDK1.8之前,HashMap采用数组+链表实现,即使用链表处理冲突(只能保证尽量分布均匀,冲突难以避免),同一hash值的节点都存储在一个链表里。但是当位于一个桶中的元素较多,链表被拉的很长,通过key值依次查找的效率大大降低。
而JDK1.8中,HashMap采用数组+链表+红黑树实现,当某个位桶的链表的长度超过 8 的时候,这个链表就将转换成红黑树,当链条长度小于6时,又会恢复成链表结构,这样大大减少了查找时间。
桶中的结构可能是链表,也可能是红黑树。
2. Get方法的原理
使用Get方法根据Key来查找Value的时候,发生了什么呢?
首先会把输入的Key做一次Hash映射,得到对应的index,贴出getNode方法的源代码:
同一个位置有可能匹配到多个Entry,这时候就需要顺着对应链表/红黑数的头节点,一个一个向下来查找。假设我们要查找的Key是“apple”:
第一步,我们查看的是头节点Entry6,Entry6的Key是banana,显然不是我们要找的结果。
第二步,我们查看的是Next节点Entry1,Entry1的Key是apple,正是我们要找的结果(当然需要判断是链表还是红黑树实现,这里以链表为例)
3. 计算存取时的index
如果节点存储分布的不均匀的话,拿取值的时候效率就会大打折扣,甚至会变成纯链表结构,复杂度由理想的O(1)变成了O(N),Hashmap设计者为了让节点存储分布的更加均匀,使用位运算的方式来计算新增节点存储在那个位置
index = HashCode(Key) & (Length - 1)
下面我们以值为“book”的Key来演示整个过程:
1.计算book的hashcode,结果为十进制的3029737,二进制的101110001110101110 1001。
2.假定HashMap长度是默认的16,计算Length-1的结果为十进制的15,二进制的1111。
3.把以上两个结果做与运算,101110001110101110 1001 & 1111 = 1001,十进制是9,所以 index=9。
但是这种方法有一个前提就是hashmap的长度必须是2的幂,要不然用这种位运算根本无法得到合适的index,甚至会超出hashmap的长度
4. ReHash
当插入的元素增加,HashMap达到一定饱和度时,Key映射位置发生冲突的几率会逐渐提高。这时候,HashMap需要扩展它的长度,也就是进行Resize。
默认hashmap的resize的条件是 hashmap的size大于或等于当前长度的百分之七十五,之后会先后执行两个步骤:
- 扩容
创建一个新的Entry空数组,长度是原数组的2倍。
- ReHash
遍历原Entry数组,把所有的Entry重新Hash到新数组。(这里一定要重写hash运算,因为以前的规则是依据hashmap的长度来的,现在长度变化了,规则也就不同了)
Resize前:
Resize后
5. 高并发下的hashmap
在jdk8之前并发的put操作可能会在rehash的时候导致成环,导致程序死循环的产生,jdk8之后没有成环的情况了(JDK 8 用 head 和 tail 来保证链表的顺序和之前一样;JDK 7 rehash 会倒置链表元素),但是还会有数据丢失问题:
- 如果多个线程同时使用 put 方法添加元素,而且假设正好存在两个 put 的 key 发生了碰撞(根据 hash 值计算的 bucket 一样),那么根据 HashMap 的实现,这两个 key 会添加到数组的同一个位置,这样最终就会发生其中一个线程 put 的数据被覆盖
2. 如果多个线程同时检测到元素个数超过数组大小 * loadFactor,这样就会发生多个线程同时对 Node 数组进行扩容,都在重新计算元素位置以及复制数据,但是最终只有一个线程扩容后的数组会赋给 table,也就是说其他线程的都会丢失,并且各自线程 put 的数据也丢失
3. 一个线程迭代时,另一个线程做插入删除操作,造成迭代的fast-fail







浙公网安备 33010602011771号