HashMap详解

173 阅读3分钟

底层实现原理:

底层实现原理是以内部类node的一个数组 在put元素时,会先调用元素的HashCode方法计算出Hash值根据这个Hash值散列找到数组中的位置如果位置为null直接赋值,如果不为null 调用equals方法判断key是否一样,不一样则插入,一样则替换

jdk1.7是使用数组加链表存储元素的,弊端就是链表中的元素过多时,查询效率下降

jdk1.8做了一个优化,当数组长度大于64,链表长度大于8时会将链表转化为红黑树,提高搜索效率

jdk1.7与jdk1.8之间的区别

1.7会有rehash操作,该操作会将所有的数据重新计算HashCode再重新赋值给一个新的HashMap,耗费时间与空间

在并发的操作HashMap时会形成环形链表和数据丢失的情况,旧链表迁移新链表的时候会发生数据倒置的情况 原因就是头插法,打乱了插入的顺序,可能发生环形链表,和数据丢失的问题,导致死循环cpu利用率100%

如何出现死循环的

假设数组中的一个链表中有元素3 2 1

有两个线程对hashmap扩容第一个线程执行到一半被挂起,第二个线程开始执行扩容,因为是头插法,链表顺序会变,但是被挂起的线程e和next元素已经确定,不会改变。第二个线程开始扩容时,新开一个newtable,开始扩容后将3赋值到新数组,3的next为newtable[i],然后将e设为2 next为2.next 是3(线程一已经修改了)将2.next赋值为newtable[i] newtable[i] = 2; 将e设为3 next = 3.next 为null 将3.next赋值为newtable[i] 此时为2 newtable[i] = 3 此时就会出现环装链表

jdk1.8对以上jdk1.7的问题做了优化

  • 经过rehash后 索引可能是在原索引或者是原索引加原数组长度,不需要像1.7那样重新计算hash值 取决于元素的HashCode在新增的哪一位上是1还是0 如果为1则是原索引加原数组长度 0则是原位置 省去了重新计算hashcode的时间
  • 发生hash碰撞不再采用头插法的方式,而是直接插入链表尾部不会出现环形链表,但是会发生数据覆盖的情况

解决hash碰撞的方法总结

  • 开放地址法

    • ThreadLocalMap 使用threadLocalHashCode 进行散列,使用开放地址法解决Hash碰撞
  • 再hash法

    • 重复使用hash方法直到没有冲突
  • 链地址法(拉链法)

    • 储存在同一线性链表中
  • 建立一个公共溢出区

并发修改异常解决方案

  • 使用HashTable 所有的方法都加上了锁,代价太大,只能有一个线程访问或操作该对象,其他线程只能阻塞
  • 使用工具类Collections.synchroizedMap(new HashMap<>())也是添加了synchronized锁
  • 写时复制 当像容器中添加元素的时候,不直接往容器中添加,而是先将当前容器的元素复制出来放到一个新容器中,新的容器添加元素,添加完之后将原来的容器引用指向新的容器,可以进行并发的读,不需要加锁,因为当前元素不添加任何元素,利用了读写分离的思想,会有内存占用 的问题,会有数据一致性的问题,只能保证数据的最终一致性,不能保证数据的实时一致性,
  • concurrentHashMap 大量使用了volatile 和 CAS 技术来减少锁竞争对性能的影响,1.7中使用分段锁思想,将数据一段一段存储,为sengment 继承了AQS天然线程安全,一个线程占用一个段时,其他段也能被访问,Hash的过程比普通hashmap长在1.8中 内部的value使用violent修饰, 使用CAS保证原子性

散列均匀分布

hashMap获取索引的indexFor方法里面的h是hashCode通过变换之后的值,是一个32位的二进制数,如果直接用如此长的二进制数和目标length-1直接进行与运算,结果会导致高位会大量丢失。

假如我们以16位为划分,任何两个高16位不一样,低16位一样的数。这两个数的hashCode与length-1做与运算(hashCode & length-1),结果会是一样的,这样的两个数,却产生了相同的hash结果,发生hash冲突。

于是hashMap想到了一种处理方式:底层算法通过让32位hashcode中保持高16位不变,高16与低16异或结果,作为新的低16位,然后用hash得到的结果(int h)传入方法indexFor获取到hashMap的索引。

计算中只有低位16位参与&运算,计算效率高,同时也保证的hash的高16位参与了索引运算,这样得到的索引能呈较为理想的散列分布,在将条目放入hashMap中时,最大限度避免hash碰撞。

static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);//把hash值异或了hash值右移16位,即取高16位 }