基础-HashMap

HashMap概括与总结

Posted by Kang on September 11, 2019

HashMap的快失败(非并发)

  HashMap中存在modCount表示一个集合被修改的次数,使用expectedModCount在创建iterator时被赋值为modCount,每次有修改时加一。
  在使用集合的操作remove、put方法操作数据时只会修改modCount,但是迭代器在变更数据时会变更modCount和同步更新expectedModCount。这样若同时存在集合变更和迭代器变更,在迭代器next方法中,则modCount!=expectedModCount就会抛出fast-fail异常。

HashMap非线程安全原因

jdk7中

  1. hash碰撞插入丢失:在并发put的情况下,两个线程都插入同一个hash值的数据,即发生hash碰撞,假如线程A刚好通过线性/树查找到自己的前驱节点后被切换到B,这样B也查找到了相同的位置,并完成了数据的存储,线程A被调度恢复后,仍在在原来的节点进行数据的插入,这样会导致B刚插入的数据丢失。
  2. resize死循环获取:HashMap的get操作可能因为resize而引起死循环(cpu100%)。线程1在resize时,刚好检查到新的结构中节点A和B是前驱与后继关系时挂起,线程2完成resize动作,恰巧反过来B和A构成了前驱和后继关系(B.next=A),此时线程1使用自己检测到的关系(A.next=B),则会构成一个环。当使用get获取的时候,将出现死循环问题。
    • 产生环的一个重要原因是:Map在扩容是通过往新链表头中添加而不是将节点追加在新链的尾部,每次新节点进来后更新的是头结点-数组节点的数据。故对于扩容前相同链表上的前后两个节点,在新节点中两者顺序将相反。推荐阅读

jdk8中

  jdk8中通过对将一个链表中的元素分别组织成低槽位和高槽位的链表,这样旧表中一个链表从全局上来看会拆分为两个链表。低槽位指针的节点放在新数组原位置[j]处,高槽位指针的节点放在新数组[原数组长度+j]处,这样破除了环的产生。
  是不是说jdk8中HashMap就是线程安全的呢?
  答案是否定的,在扩容时,do while循环进行链表拼接时,由于不是原子操作,会导致拼接覆盖的情况,导致数据丢失。

参数选择

底层数据为何需要向2的N次方对齐?

须知:在计算机中,运算效率为:加法>乘法>除法>取模>取余

  在Map结构中,去查找当前元素的位置存在两步:

  1. 第一步是对数组长度进行整形,将其整理为2的N次方。
  2. 第二步再定位到数组中的位置,也即hashcode对数组长度取余。
      对于取余,当数组长度为2的N次幂的时候,与位运算存在关系:
    1
    
    hashCode % length = hashCode & (length-1)
    

    举例,假设数据长度length = 16,则与运算如下:

    1
    2
    
    hashCode : 1001 1100 1100 0011
    length-1 : 0000 0000 0000 1111  ##(16-1 = 15) 
    

    从上面可以看出,任何的hashCode与15(1111)相与之后,一定落在0~15之间,也即在数组长度范围内。这也是为啥底层为了加快数据的定位,使用了位运算来代替取余,同时一定要向2的N次方对齐的最主要原因。

DEFAULT_LOAD_FACTOR为何为0.75?

  HashMap类定义之上有解释:负载因子太小了浪费空间并且会发生更多次数的resize,太大了哈希冲突增加会导致性能不好,所以0.75只是一个折中的选择。

为何设置链表长度为8时转换为红黑树?

  理想状态下,随机哈希的存放数据方式本身满足泊松分布的,也即put一个值到节点A中的时候,之后的一个put同样放在A中的概率遵循<泊松分分布>。在负载因子为0.75的时候,HashMap中按照泊松分布的计算公式计算出了链表中元素个数和概率的对照表,可以发现一个bin中的链表长度超过8的概率为0.00000006,即链表中元素个数为8时的概率已经非常小,所以转化为红黑树也在很少情况下,在该长度下也能更好的发挥树的优势。

  通过上面的分析可知,其实转化为红黑树概率比较小,JDK8相对JDK7来说,其实其整体效率提升有限,只提高了8%~10%左右。