https://zhuanlan.zhihu.com/p/76735726
https://juejin.im/post/6844903921190699022#heading-0
https://tech.meituan.com/2016/06/24/java-hashmap.html
jdk8后采用数组+链表+红黑树的数据结构,利用元素的key的hash值对数组长度取模得到在数组上的位置。当出现hash值一样的情形,就在数组上的对应位置形成一条链表。据碰撞越来越多大于8的时候,就会把链表转换成红黑树。
https://blog.csdn.net/qq_38182963/article/details/78942764
1.Key.hashCode和无符号右移16位做异或运算得到hash值,取模运算计算下标index
对key的hashCode 和右移16位做异或运算,之后hash(key) & (capacity - 1)做按位与运算得到下标。
2.下标的位置没有元素说明没有发生碰撞,直接添加元素到散列表中去
3.如果发生了碰撞(hash值相同),进行三种判断
4.1:若key地址相同或equals相同,则替换旧值
4.2:key不相等,如果是红黑树结构,就调用树的插入方法
4.3:key不相等,也不是红黑树,循环遍历直到链表中某个节点为空,用尾插法(1.8)/头插法(1.7)创建新结点插入到链表中,遍历到有节点哈希值相同则覆盖,如果,链表的长度大于等于8了,则将链表改为红黑树。
4.如果桶满了大于阀值,则resize进行扩容
1.Key.hashCode的高16位做异或运算得到hash值,取模运算计算下标index
2.找到所在的链表的头结点,遍历链表,如果key值相等,返回对应的value值,否则返回null
1.多线程put的时候可能导致元素丢失
2.put非null元素后get出来的却是null
3.多线程扩容,引起的死循环问题
1.put的时候会根据tab[index]是否为空执行直接插入还是走链表红黑树逻辑, 并发时,如果两个put 的key发生了碰撞,同时执行判断tab[index]是否为空,两个都是空,会同时插入,就会导致其中一个线程的 put 的数据被覆盖。
2.元素个数超出threshold扩容会创建一个新hash表,最后将旧hash表中元素rehash到新的hash表中, 将旧数组中的元素置null。线程1执行put时,线程2执行去访问原table,get为null。
3.在扩容的时候可能会让链表形成环路。原因是会重新计算元素在新的table里面桶的位置,而且还会将链表翻转过来。 多线程并发resize扩容,头插造成了逆序A-B 变成了C-B ,t1执行e为A,next为B挂起, t2执行完毕导致B指向A,继续执行t1,他继续先头插eA,再头插nextB, 由于t2程导致B后面有A,所以继续头插, A插到B前面,出现环状链表。 get一个在这个链表中不存在的key时,就会出现死循环了。
https://juejin.im/post/6844903796225605640#heading-5
https://coolshell.cn/articles/9606.html/comment-page-3#comments
https://www.iteye.com/blog/firezhfox-2241043
参考: https://blog.csdn.net/qq_36520235/article/details/82417949
由数组+链表的结构改为数组+链表+红黑树。
优化了高位运算的hash算法:h^(h>>>16)
扩容后,元素要么是在原位置,要么是在原位置再移动2次幂的位置,且链表顺序不变。
头插改为尾插
(1)由 数组+链表 的结构改为 数组+链表+红黑树 。
拉链过长会严重影响hashmap的性能, 在链表元素数量超过8时改为红黑树,少于6时改为链表,中间7不改是避免频繁转换降低性能。
(2) 优化了高位运算的hash算法
h^(h>>>16)将hashcode无符号右移16位,让高16位和低16位进行异或。
(3)扩容 扩容后数据存储位置的计算方式也不一样
1.7是直接用hash值和需要扩容的二进制数进行与操作,1.8(n-1)&hash,位运算省去了重新计算hash,只需要判断hash值新增的位是0还是1,0的话索引没变,1的话索引变为原索引加原来的数组长度 ,且链表顺序不变。
(4)JDK1.7用的是头插法,而JDK1.8及之后使用的都是尾插法。
因为JDK1.7是用单链表进行的纵向延伸,当采用头插法时会容易出现逆序链表形成环路导致死循环问题。 但是在JDK1.8之后是因为加入了红黑树,使用尾插法,能够避免出现逆序且链表死循环的问题。
链表取元素是从头结点一直遍历到对应的结点,这个过程的复杂度是O(N) , 而红黑树基于二叉树的结构,查找元素的复杂度为O(logN) , 所以,当元素个数过多时,用红黑树存储可以提高搜索的效率。
红黑树虽然查询效率比链表高,但是结点占用的空间大,treenodes的大小大约是常规节点的两倍 只有达到一定的数目才有树化的意义,这是基于时间和空间的平衡考虑。 如果一开始就用红黑树结构,元素太少,新增效率又比较慢,无疑这是浪费性能的。
https://blog.csdn.net/T_yoo_csdn/article/details/87163439
但是二叉查找树在特殊情况下会变成一条线性结构
如果构建根节点以后插入的数据是有序的,那么构造出来的二叉搜索树就不是平衡树,而是一个链表,它的时间复杂度就是 O(n),遍历查找会非常慢。
红黑树,每次更新数据以后再进行平衡,以此来保证其查找效率。
容器中节点分布在hash桶中的频率遵循泊松分布
各个长度的命中概率依次递减,源码注释中给我们展示了1-8长度的具体命中概率。
当长度为8的时候,概率概率仅为0.00000006,这么小的概率,大于上千万个数据时HashMap的红黑树转换几乎不会发生。
主要是一个过渡,避免链表和红黑树之间频繁的转换。 如果一个HashMap不停的插入、删除元素,链表个数在8左右徘徊, 就会频繁的发生树转链表、链表转树,效率会很低。
(1)开放定址法 (2)链地址法 (3)再哈希法 (4)公共溢出区域法
如果bucket满了(超过load factor*current capacity),就要resize。
为什么负载因子是0.75 小于0.5,空着一半就扩容了, 如果是0.5 , 那么每次达到容量的一半就进行扩容,默认容量是16, 达到8就扩容成32,达到16就扩容成64, 最终使用空间和未使用空间的差值会逐渐增加,空间利用率低下。
当负载因子是1.0的时候, 出现大量的Hash的冲突时,底层的红黑树变得异常复杂。对于查询效率极其不利。这种情况就是牺牲了时间来保证空间的利用率。
是0.75的时候
空间利用率比较高,而且避免了相当多的Hash冲突,使得底层的链表或者是红黑树的高度比较低,提升了空间效率。
对key的hashCode 和右移16位做异或运算,之后hash(key) & (capacity - 1)做按位与运算得到下标。
Hash函数是指把一个大范围映射到一个小范围。把大范围映射到一个小范围的目的往往是为了节省空间,使得数据容易保存。
如果不同的输入得到了同一个哈希值,就发生了"哈希碰撞"(collision)。
比较出名的有MurmurHash、MD4、MD5等等。
均匀散列表的下标,降低hash冲突的几率。 不融合高低位,hashcode返回的值都是高位的变动的话,造成散列的值都是同一个。 融合后,高位的数据会影响到 index 的变换,依然可以保持散列的随机性。 打个比方,当我们的length为16的时候,哈希码(字符串“abcabcabcabcabc”的key对应的哈希码)对(16-1)与操作,对于多个key生成的hashCode,只要哈希码的后4位为0,不论不论高位怎么变化,最终的结果均为0。 扰动函数优化后:减少了碰撞的几率。
%运算不如位移运算快
在 B 是 2 的幂情况下:A % B = A & (B - 1)
和这个(n - 1) & hash的计算方法有着千丝万缕的关系 按位与&的计算方法是,只有当对应位置的数据都为1时,运算结果也为1,当HashMap的容量是2的n次幂时,(n-1)的2进制也就是1111111***111这样形式的,这样与添加元素的hash值进行位运算时,能够充分的散列,使得添加的元素均匀分布在HashMap的每个位置上,减少hash碰撞。
例如长度为8时候,3&(8-1)=3 2&(8-1)=2 ,不同位置上,不碰撞。
而长度为5的时候,3&(5-1)=0 2&(5-1)=0,都在0上,出现碰撞了
HashMap 如果完全不存在冲突则 通过 key 获取 value 的时间复杂度就是 O(1), 如果出现哈希碰撞,HashMap 里面每一个数组(桶)里面存的其实是一个链表,这时候再通过 key 获取 value 的时候时间复杂度就变成了 O(n), HashMap 当一个 key 碰撞次数超过8 的时候就会把链表转换成红黑树,使得查询的时间复杂度变成了O(logN)。 通过高16位异或低16位运算降低hash冲突几率。