底层实现原理:
底层实现原理是以内部类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位 }