我需要简单地说一下HashMap这个经典的数据结构。
HashMap通常会用一个指针数组(假设为table[])来做分散所有的key,当一个key被加入时,会通过Hash算法通过key算出这个数组的下标i,然后就把这个 插到table[i]中,如果有两个不同的key被算在了同一个i(因为一些值的hash算法的结果值是一样的,所以以下标也就一样了),那么就叫冲突,又叫碰撞,这样会在table[i]上形成一个链表。
我们知道,如果table[]的尺寸很小,比如只有2个,如果要放进10个keys的话,那么碰撞非常频繁,于是一个O(1)的查找算法,就变成了链表遍历,性能变成了O(n),这是Hash表的缺陷。
所以,Hash表的尺寸和容量非常的重要。一般来说,Hash表这个容器当有数据要插入时,都会检查容量有没有超过设定的thredhold,如果超过,需要增大Hash表的尺寸,但是这样一来,整个Hash表里的元素都需要被重算一遍。这叫rehash,这个成本相当的大。
rehash就是说当hash的size不够时,就会进行扩容,扩容的时候需要重新计算元素的数组下标
1、重新分配一个新的Entry数组
2、重新计算原来元素的在新数组中的下标(比较耗资源)
正常rehash过程 :
数据准备
在size=2的HashMap
中按照顺序添加5, 7, 3这三个key,假设按照mod 2的算法来计算元素数组下标,那么key 5,7,3都会落在下标为1的数组桶中(发生hash冲突),如下图:
把HashMap的size扩容为4后,正常rehash的过程
所以这是一个正常的rehash过程了。
并发下的rehash过程
当两个并发线程thread1和thread2都同时进入到transfer时,也即是,刚好thread1和thread2都要对HashMap进行扩容,万一这个时候thread1执行下面的代码时,被线程调度器挂起了,而thread2则正常的把扩容的操作做完。
这个时候对于thread1的情况是:
这个时候,thread1拥有执行权限了,则继续它的扩容操作,等thread1扩容完后就产生了一个环形链表了。
这时候thread1一半时是:
但是这时候hash里key(7).next 已经指向了key(3),线程2那里已经有指向了,所以,这时候的结构是:
这个时候,如果有个get请求,就有可能发生死循环,一直在链表中绕来绕去的,没法终止。
next一直在key(3)、key(7)中获取。
所以以后在分布式中,还是用ConcurrentHashMap或是HashTable。