赞
踩
上一章有写risize()代码,这一章我们从risize()开始进行讲解。
新建一个更大尺寸的 hash 表,然后把数据从老的 Hash 表中迁移到新的 Hash 表中
resize#源码:
- void resize(int newCapacity) {
- Entry[] oldTable = table;
- int oldCapacity = oldTable.length;
- ......
- // 创建一个新的 Hash Table
- Entry[] newTable = new Entry[newCapacity];
- // 将 Old Hash Table 上的数据迁移到 New Hash Table 上
- transfer(newTable);
- table = newTable;
- threshold = (int)(newCapacity * loadFactor);
- }
resize#方法中引用了transfer#方法,transfer#方法源码:
- void transfer(Entry[] newTable) {
- Entry[] src = table;
- int newCapacity = newTable.length;
- //下面这段代码的意思是:
- // 从OldTable里摘一个元素出来,然后放到NewTable中
- for (int j = 0; j < src.length; j++) {
- Entry<K,V> e = src[j];
- if (e != null) {
- src[j] = null;//释放旧Entry数组的对象引用(for循环后,旧的Entry数组不再引用任何对象;方便垃圾回收
- do {
- Entry<K,V> next = e.next;
- int i = indexFor(e.hash, newCapacity);//!!重新计算每个元素在数组中的位置
- e.next = newTable[i];//标记[1]
- newTable[i] = e;//将元素放在数组上
- e = next; //访问下一个Entry链上的元素
- } while (e != null);
- }
- }
- }
该方法实现的机制就是将每个链表转化到新链表,并且链表中的位置发生反转,而这在多线程情况下是很容易造成链表回路,从而发生 get() 死循环。所以只要保证建新链时还是按照原来的顺序的话就不会产生循环(JDK 8 的改进)
我们先来看下单线程情况下,正常的rehash过程
1、假设我们的hash算法是简单的key mod一下表的大小(即数组的长度)。
2、最上面是old hash表,其中HASH表的size=2,所以key=3,5,7在mod 2 以后都冲突在table[1]这个位置上了。
3、接下来HASH表扩容,resize=4,然后所有的<key,value>重新进行散列分布,过程如下:
(1)假设有两个线程
do {
Entry<K,V> next = e.next; // 假设线程一执行到这里就被调度挂起了
int i = indexFor(e.hash, newCapacity);
e.next = newTable[i];
newTable[i] = e;
e = next;
} while (e != null);
注意: 这里非常重要
e.next = newTable[i];
newTable[i] = e;
e = next;这三行代码解释:
将newTable[i]上的值赋给e元素的next属性(如果是第一次循环中,newTable[i]是null,即第一个元素的next节点赋值为null),e属性再赋值给newTable[i],这样newTable[i]上的链表新node都会靠前,之前的元素相当于后移了。这样链表的顺序就倒过来了,就是我们说的前插法。
而线程二执行完成了。于是有下面的这个样子
注意,因为 Thread1 的 e 指向了 key(3),而 next 指向了 key(7),其在线程二 rehash 后,指向了线程二重组后的链表。可以看到链表的顺序被反转
(2)线程一被调度回来执行
(3)线程一接着工作。把 key(7) 摘下来,放到 newTable[i] 的第一个,然后把 e 和 next 往下移
4)环形链接出现
e.next = newTable[i] 导致 key(3).next 指向了 key(7)
此时的 key(7).next 已经指向了 key(3), 环形链表就这样出现了
疑问:线程一被调度回来执行时,第一次执行e.next = newTable[i]
时; 此时不应该是 e.next = 7
吗?
如上图,此时newTable[i]
3的位置上,不是已经有值了?我感觉,这段代码,会一直死循环下去;
答: 这个在上面解释三行代码的时候已经解释过了。网上另有答案,与我写的是一个意思:newTable[]
是在每个线程里new
出来的,属于线程私有的;所以,第一次,里面并没有值(或者是随机的值);
疑问:循环产生的原因:
答:我是这么认为的,之所以会产生这种死循环,①是每次扩容时,链表都发生了倒置,导致原先的next
都存的是它们前一个元素。为发生环形链表埋下伏笔;②链表插入的方式,其采用的是头插入,每次插入新元素,都是放在最前面的; 不过也正式因为其采用头插入,所以才会发送了链表倒置;这是不是告诉我,假设我以后用到链表时,最好不要用头插入的方式,来新增元素。
JDK 8 中采用的是位桶 + 链表/红黑树的方式,当某个位桶的链表的长度超过 8 的时候,这个链表就将转换成红黑树
HashMap 不会因为多线程 put 导致死循环(JDK 8 用 head 和 tail 来保证链表的顺序和之前一样;JDK 7 rehash 会倒置链表元素),但是还会有数据丢失等弊端(并发本身的问题)。因此多线程情况下还是建议使用 ConcurrentHashMap
HashMap 在并发时可能出现的问题主要是两方面:
如果多个线程同时使用 put 方法添加元素,而且假设正好存在两个 put 的 key 发生了碰撞(根据 hash 值计算的 bucket 一样),那么根据 HashMap 的实现,这两个 key 会添加到数组的同一个位置,这样最终就会发生其中一个线程 put 的数据被覆盖
如果多个线程同时检测到元素个数超过数组大小 * loadFactor,这样就会发生多个线程同时对 Node 数组进行扩容,都在重新计算元素位置以及复制数据,但是最终只有一个线程扩容后的数组会赋给 table,也就是说其他线程的都会丢失,并且各自线程 put 的数据也丢失
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。