内部数据结构
JDK1.8:数据+链表红黑树;
当链表长度>8且数组大小大于等于64转换为红黑树。
当红黑树节点个数小于6转换为链表。
数据插入原理
1. 开始插入
2. if 数组为空
3. 初始化数组
4. 计算存储位置(n-1)&hash(k),计算数组下标
5. if 指定位置存在数据
6. if key相等
7. 覆盖value返回旧值
8. else
9. if 数组当前节点为红黑树节点
10. 放入红黑树节点
11. else
12. 放入链表
13. if 链表节点数大于8
14. 转换为红黑树
15. else
16. 存放节点
17. if 节点数大于阈值
18. 扩容为原数组的二倍
19. 结束
初始容量大小
如果 new HashMap()不传值,默认大小为16,负载因子是0.75(在时间和空间均衡考虑)。
如果传入初始大小k,初始化为大于k的2的整数次方。传入10大小为16。
哈希函数
先拿到key.hashcode()得到的32位int值,然后将高16位进而低16位进行异或操作。(扰动函数)
设计的原因:
为了降低hash碰撞,越分散越好。
算法要尽量高效,所以采用位运算。
为什么不直接使用key.hashcode呢?
因为返回的结果是32位int值-2147483648~2147483647,40亿多的映射空间。很难出现碰撞,但是数组过长,内存放不下。再对数组长度-1取模(源码实现为与操作,数组长度N为2的整数次方,N-1是相当于一个全1的掩码)才能访问数组下标。这样尽管散列值很松散,但是取最后几位的话,碰撞还是可能会很严重,如果恰好散列本身做的不好,分布上成等差数列的漏洞,正好出现最后个低位呈现规律性重复,就更麻烦了。
所以引入扰动函数,高半区和低半区做异或,就可以保留高位的信息,加大了低位的随机性。(用右移和异或来计算)
在这里JDK1.7做了四次移位和四次异或,但Java 8只做一次,做4次的话,多了可能边际效用也不大,所谓为了效率考虑就改成一次了。
JDK1.8的优化
数组+链表改成了数组+链表或红黑树
使用红黑树防止hash冲突导致链表过长,时间复杂度降为O(logn)
链表的插入方式从头插法改成了尾插法
简单说就是插入时,如果数组位置上已经有元素,1.7将新元素放到数组中,原始节点作为新节点的后继节点,1.8遍历链表,将元素放置到链表的最后;
1.7时头插法扩容时重新hash放置元素时链表反转,多线程环境下会产生环。
扩容1.7重新hash定位,1.8位置不变或索引+旧容量大小。
扩容相当于掩码多了1位。如果原数据这一位为0则不变,为1则多一个旧容量大小。
1.7重新定位,1.8用与16的结果划分为低半和高位。
在插入时,1.7先判断是否需要扩容,再插入,1.8先进行插入,插入完成再判断是否需要扩容;
HashMap不是线程安全的
1.7 会产生死循环、数据丢失、数据覆盖的问题。
死循环:因为用头插法,扩容的时候链表会倒置,并发情况下倒置两次,形成环路。
1.8 中会有数据覆盖的问题。
A线程判断index为空挂起,B向index写入,A恢复现场赋值。覆盖发生。
如何解决线程不安全问题
Java中有HashTable、Collections.synchronizedMap、以及ConcurrentHashMap可以实现线程安全的Map。
HashTable是直接在操作方法上加synchronized关键字,锁住整个数组,锁粒度比较大。
Collections.synchronizedMap是使用Collections集合工具的内部类,通过传入Map封装出一个SynchronizedMap对象,内部定义了一个对象锁,方法通过对象锁实现线程安全。
ConcurrentHashMap使用分段锁,降低了锁粒度,让并发度大大提高。(再深入了解一下)
链表红黑树互转问题
链表转红黑树的阈值为(大于)8(先创建节点后转,实际上9的时候才转)。
在hash函数设计合理的情况下hash碰撞8次的几率为百万分之6。8基本够用了。
但是并不是过8就转,还要看数组是否小于64,不到64先扩容。大于等于64才转红黑树。
红黑树转链表的阈值是(小于)6。为了避免碰撞次数在8附近徘徊时一直发生互相转化,所以设置为6。
HashMap内部节点无序
HashMap根据hash值随机插入,不保存插入顺序。
LinkedHashMap和TreeMap是有序的。
LinkedHashMap内部维护了一个单链表,有头尾节点。在HashMap基础上增加了before和after标记前置节点和后置节点,可以实现按插入顺序或访问顺序排序。
TreeMap是按照Key的顺序或者比较函数的顺序进行排序。内部通过红黑树实现。