链表的插入方式从头插法改成了尾插法,简单来说就是插入时,如果数组位置上已经有元素,1.7将新元素放到数组中, 原始节点作为新节点的后继节点,1.8则是遍历链表将元素放到链表的最后。
数据结构由数组+链表改成了数组+链表或红黑树。
扩容时1.7需要对原数组中的元素进行重新hash定位在新数组的位置,1.8采用更简单的判断逻辑,位置不变或索引+旧容量大小。
在插入时,1.7先判断是够需要扩容,再插入,1.8先插入,插入完成后再判断是够需要扩容。
为什么要把头插法改成尾插法?
在多线程场景下,头插法可能会出现链表形成环。比如T1、T2两个线程在插入元素时遇到了扩容场景,恰好此时T1线程让出时间片,T2线程会率先进行扩容,链表中的元素因为是采用头插法,扩容后新的链表元素和原链表元素的顺序是相反的,T2线程扩容结束后,T1线程被唤醒,但是T1线程对链表元素顺序被修改无感知,T1线程中链表元素的顺序与新的链表中的元素顺序完全相反,就形成了一个环。其实主要还是因为线程不安全问题,共享变量值的改变没有让每个线程都感知到。
而尾插法,在扩容时会保持链表元素原本的顺序,就不会出现链表成环的问题。
但是即使改成尾插法仍然会存在其他多线程安全,比如:上秒 put 的值,下秒 get 的时候却不是刚 put 的值;因为操作都没有加锁,不是线程安全的。所以在多线程场景下不要使用HashMap。
扩容的时候为什么1.8不用重新hash就可以直接定位原节点在新数组中的位置呢?
为啥扩容两倍?原因就在此,为了不用重新 hash (散列) 。
情况1:【推导过程1】当e.hash&oldCap等于0时,e在新旧数组中的索引位置不变:(2oldCap-1)& e.hash = (oldCap -1) & e.hash。
情况2:【推导过程2】当e.hash&oldCap不等于0的元素,e在新数组中的索引位置是其在旧数组中索引位置的基础上再加上旧数组长度个偏移量: (2oldCap-1)& e.hash = (oldCap -1) &e.hash+oldCap
[分析] 假设一个元素经过hash()运算得到值是hash,其在数组的桶是 (oldCap - 1) & hash位置。
假设oldCap=8,hash=9。
旧数组:0111 & 1001 = 0001 = 1,元素在1位置。
此时扩容两倍,cap=16。
新数组:e.hash & oldCap 不等于0(1001 & 1000 = 1000 = 8),1111 & 1001 = 01001 = 9,元素在9位置。相当于 1 + 8。
例如:oldCap=8,hash=33。
旧:0111 & 0010 0001 = 0001 = 1,元素在1位置。
此时扩容两倍,cap=16。
新:e.hash & oldCap等于0 (100001 & 10000 = 0),1111 & 0010 0001 = 0001= 1,所以元素还在原位置。
本文作者:whitebear
本文链接:
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!