HashMap的数据结构:
HashMap是用一个指针数组(假设为table[])来做分散所有的key,当一个key被加入时,会通过Hash算法通过key算出这个数组的下标i,然后就把这个<key, value>插到table[i]中,如果有两个不同的key被算在了同一个i,那么就叫冲突,又叫碰撞,这样会在table[i]上形成一个链表。我们知道,如果table[]的尺寸很小,比如只有2个,如果要放进10个keys的话,那么碰撞非常频繁,于是一个O(1)的查找算法,就变成了链表遍历,性能变成了O(n),这是Hash表的缺陷。
所以,Hash表的尺寸和容量非常的重要。一般来说,Hash表这个容器当有数据要插入时,都会检查容量有没有超过设定的阈值thredhold,如果超过,需要增大Hash表的尺寸,但是这样一来,整个Hash表里的无素都需要被重算一遍。这叫rehash,这个成本相当的大。(参考:疫苗:JAVA HASHMAP的死循环))
参考文章:HashMap的为啥用尾插法?
JDK1.7的时候使用的是数组加链表的形式存储数据。在发生哈希碰撞的时候,就会产生一个节点,并且将这个节点添加在链表的表头,也就是头插法。在单线程的时候没有问题,但是并发的情况下多线程同时进行put操作,并且同时进行扩容的时候可能会出现链表环,导致死循环的发生。
先说下扩容:扩容分为两步:
- 扩容:创建一个新的Entry空数组,长度是原数组的2倍。
- ReHash:遍历原Entry数组,把所有的Entry重新Hash到新数组。
因为扩容之后,数组的长度就变化了。然后根据hash的公式:index = HashCode(Key) & (Length - 1) 如果后面新添加元素,那就和之前的元素使用的hash函数不一样了。所以需要重新计算hash。重新计算索引位置之后,有可能原来在一个Entry链上的元素被放在了不同的位置。这时候如果是多线程的话就会形成A->B->A这样的循环链表的情况。这个时候去取值就会出现无限循环的状态。
那么JDK1.8之后为什么使用尾插法呢?
使用尾插法在扩容时会保持链表元素原本的顺序。Java8在同样的前提下并不会引起死循环,原因是扩容转移后前后链表顺序不变,保持之前节点的引用关系。
那是不是意味着Java8就可以把HashMap用在多线程中呢?
我认为即使不会出现死循环,但是通过源码看到put/get方法都没有加同步锁,多线程情况最容易出现的就是:无法保证上一秒put的值,下一秒get的时候还是原值,所以线程安全还是无法保证。