先总览下Map继承体系:
HashMap
1、继承体系
HashMap继承了AbstractMap并实现了Map,Cloneable,Seriliazable接口
2、重要参数
初始/最大的数组大小 (initial_capacity) 初始 2^4 = 16 最大 2^30,10亿多 | |
初始负载因子(load_factor) 初始:0.75 | ![]() |
树化链表及数组的阈值 treeify 1、链表长度至少为8 2、数组容量至少为64(链表阈值的四倍64) | ![]() |
树退化为链表(untreeify_threshold)的阈值 红黑树长度至多为(<=) 6时 | ![]() |
哈希表 | ![]() |
HashMap 的实际大小,可能不准(因为当你拿到这个值的时候,可能又发生了变化) | ![]() |
扩容的门槛(threshold),有两种情况: 1、如果初始化时,给定数组大小的话,通过 tableSizeFor 方法计算,数组大小永远接近于 2 的幂次方,比如你给定初始化大小 19,实际上初始化大小为 32,为 2 的 5 次方。 2、如果是通过 resize 方法进行扩容,threshold= 数组容量 * 0.75 | ![]() |
负载因子 threshold = capacity * loadFactory | ![]() |
当前哈希表结构修改次数 | ![]() |
3、链表Node结点数据结构
4、红黑树的结点
5、HashMap底层存储结构图示
HashMap重点梳理~
重点1:put数据原理分析
如 :put("x","y")
1、获取“x”字符串的Hash值,经过Hash值的扰动函数,使Hash值更散列;
2、构造出Node对象;
3、由路由算法,找出node应存放在数组的位置;
PS:路由址公式:(table.length-1) & node.hash 求得存放的数组下标;
table.length必然是2的倍数,table.length-1(相当于一个低位掩码)的二进制形式必定是类似:00…000 1111 的形式进行与运算,前边都是0高位不参与运算;
Hash()方法即扰动函数,作用是:让hash值的高16位也参与路由运算
扰动函数:https://www.javacodegeeks.com/2015/09/an-introduction-to-optimising-a-hashing-strategy.html
2进制32位带符号的int表值范围从-2147483648到2147483647,只要哈希函数映射得比较均匀松散,一般应用是很难出现碰撞的;
但是前后40多亿的映射空间,对于初始值数组长度为16的HashMap来说,是绝对放不下的,因此,提供一个“低位掩码”(table.length-1) 与上 hash值,即路由算法,
得到的低位值用来作数组下标;这时,新的问题产生,即便散列值分布的再松散,只去后几位,碰撞也会很严重,扰动函数:
混合原始哈希码的高位和低位,以此来加大低位的随机性,尽量做到任何一位的变化都能对最终得到的结果产生影响
352个字符串,在他们散列值完全没有冲突的前提下,对它们做低位掩码,取数组下标
重点2:什么是hash碰撞?
hash碰撞不可避免,因为不同的hash值转化成二进制(如1122的二进制 000..0 0100 0110 0010 与 3170的二进制 000..0 1100 0110 0010),
由于后四位相同,但高位不进行运算,进行寻址操作时得到的下标地址相同,这就造成冲突;
Hash碰撞带来的问题,理想情况下查找的时间复杂度为O(1),但是如果链化的很严重,相当于退化到了O(n)
重点3:什么是链化?为什么引入红黑树?
由于Hash碰撞的问题引入链化,为了解决链化严重的问题引入红黑树;
重点4:HashMap扩容门槛及原理
扩容的门槛(threshold),有两种情况:
1、如果初始化时,给定数组大小的话,通过 tableSizeFor 方法计算,数组大小永远接近于 2 的幂次方,比如你给定初始化大小 19,实际上初始化大小为 32,为 2 的 5 次方。
2、如果是putVal()触发,通过 resize 方法进行扩容,threshold= 数组容量 * 0.75
new HashMap() 实例化时 tableSizeFor() | ![]() |
putVal时触发扩容 Resize() | 扩容,解决hash冲突导致链化严重的问题! 0、判断是否为空 1、判断是否只有一个元素,直接插入 2、判断是否是树化 3、判断是否形成链表 |
重点5:HashMap Jdk1.7 于 1.8对比!
头插法 尾插法
JDK1.7 | 在高并发下可能会出现 扩容死锁及数据丢失的问题! 在扩容时,扩容是创建一个新的数组,然后旧数组数据进行转移!问题发生在resize()时->transfer()方法,多个线程同事对一个数组扩容,导致链表发生环链,导致死循环,cpu占有率可能飙升至100%! |
JDK1.8 | 改进措施是在resize()方法的一个分支里,使用了四个指针,两个高位、两个低位指针,通过hashcdoe & 旧数组的长度 得到的值只有两种情况1、等于0,此时放入低位指针 2、等于旧数组长度,放入高位指针! |
Hashtable (注意t小写哦)
打开源码发现除了构造外几乎所有方法都加了synchronized关键字:
注意到类开头的注释:
Hashtable已经不被推荐,如果不要求线程安全请使用HashMap;如果有线程安全需求,有效率更高的ConcurrentHashMap!
总结:本质上是线程安全的HashMap,跟Vector一样,都是用Synchronized修饰方法!