开篇明志
和前一片文章《JDK源码-HashMap》的写作目的差不多,在创作ConcurrentHashMap这片文章的时候,需要和HashMap做对比,《JDK源码-HashMap》着重介绍了内部实现原理,但是没有详细说明为什么HashMap为什么会发生死锁现象——导致它是非线程安全的。
一、HashMap底层实现
这块更详细的内容请移步《JDK源码-HashMap》
简单的可以从以下两个纬度去理解HashMap的底层实现原理。
- 数组:充当索引
- 链表:处理碰撞
HashMap用一个指针数组table,离散化key的作用,当加入一个 key 的时候,通过Hash算法,计算出 key所在的数组下标 i,如果table[i]位置的对象元素为null的时候,则直接将<key, value>
加入即可;但是,如果table[i]位置已经被占用的话,则会发生冲突碰撞;此时,会在 table[i]上形成一个链表。
如果table太小,就会发生频繁碰撞;此时,查询时间复杂度由O(1)变为O(n).
因此,Hash 表的尺寸和容量非常重要。每次当有新的数据要插入Hash 表时,都会检查容量有没有超过 thredshold,如果超过,需要扩容 Hash 表,这需要改变重新计算hash分桶的位置—— rehash,这种操作是比较耗时的。
所以,在创建HashMap实例的时候需要预先估计一下需要处理的数据量的大小,提前将table的大小和装载因子load factor设置好,减少Hash碰撞的概率,同时也可以减少扩容hash表的次数,达到节约时间的目的。
二、源码阅读
根据前面的文章《JDK源码-HashMap》可以知道,当每次添加新元素都是在链表头部添加元素,那么,问题来了——为什么会造成死锁呢?按理说每次在链表头部添加元素的话,不可能出现死锁现象的。
问题就出在rehash过程,当将旧table元素转移到新的newTable的时候,我们一块来看看transfer()函数的源码,分析一下原因。
transfer()源码如下:
/** * Transfers all entries from current table to newTable. */
void transfer(Entry[] newTable, boolean rehash) {
int newCapacity = newTable.length;
//Step1 : 首先便利索引数组中的元素,Entry<K,V> e 存储了链表的入口元素
for (Entry<K,V> e : table) {
//Step2: 对链表上的每一个元素进行遍历,从Hash表的头部第一个元素开始
while(null != e) {
Entry<K,V> next = e.next;
if (rehash) {
e.hash = null == e.key ? 0 : hash(e.key);
}
int i = indexFor(e.hash, newCapacity);
e.next = newTable[i];
newTable[i] = e;
e = next;
}
}
}
总结:
- 首先便利索引数组中的元素,获取到链表的入口节点
- 对链表上的每一个节点遍历:先将 e.next 指向新 Hash 表的第一个元素(如果是第一次就是 null),这时候新 Hash 的第一个元素是 e,但是 Hash 指向的却是 e 没转移时候的第一个,所以需要将 Hash 表的第一个元素指向 e.
- while循环遍历链表节点,直到全部转移到新的newTable
- for循环遍历table,可以理解为链表的入口头节点,直到所有索引数组全部转移到新的newTable
可以看到转移过程是逆序的,转移前链表顺序是1->2->3,逆序转移后新的t顺序变成 3->2->1。现在就应该才得八九不离十了,是不是有可能在转移的过程中 出现1>2>3>1这种情况,形成一个头尾相连的链表。