谈谈HashMap线程不安全的体现

HashMap的原理以及如何实现,之前在 JDK7与JDK8中HashMap的实现 中已经说明了。

那么,为什么说HashMap是线程不安全的呢?它在多线程环境下,会发生什么情况呢?

1. resize死循环

我们都知道HashMap初始容量大小为16,一般来说,当有数据要插入时,都会检查容量有没有超过设定的thredhold,如果超过,需要增大Hash表的尺寸,但是这样一来,整个Hash表里的元素都需要被重算一遍。这叫rehash,这个成本相当的大。

  1. void resize(int newCapacity) {
  2. Entry[] oldTable = table;
  3. int oldCapacity = oldTable.length;
  4. if (oldCapacity == MAXIMUM_CAPACITY) {
  5. threshold = Integer.MAX_VALUE;
  6. return;
  7. }
  8. Entry[] newTable = new Entry[newCapacity];
  9. transfer(newTable, initHashSeedAsNeeded(newCapacity));
  10. table = newTable;
  11. threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
  12. }
复制代码
  1. void transfer(Entry[] newTable, boolean rehash) {
  2. int newCapacity = newTable.length;
  3. for (Entry<K,V> e : table) {
  4. while(null != e) {
  5. Entry<K,V> next = e.next;
  6. if (rehash) {
  7. e.hash = null == e.key ? 0 : hash(e.key);
  8. }
  9. int i = indexFor(e.hash, newCapacity);
  10. e.next = newTable[i];
  11. newTable[i] = e;
  12. e = next;
  13. }
  14. }
  15. }
复制代码

大概看下transfer:

对索引数组中的元素遍历

对链表上的每一个节点遍历:用 next 取得要转移那个元素的下一个,将 e 转移到新 Hash 表的头部,使用头插法插入节点。

循环2,直到链表节点全部转移

循环1,直到所有索引数组全部转移

经过这几步,我们会发现转移的时候是逆序的。假如转移前链表顺序是1->2->3,那么转移后就会变成3->2->1。这时候就有点头绪了,死锁问题不就是因为1->2的同时2->1造成的吗?所以,HashMap 的死锁问题就出在这个 transfer() 函数上。

1.1 单线程 rehash 详细演示

单线程情况下,rehash 不会出现任何问题:

假设hash算法就是最简单的 key mod table.length(也就是数组的长度)。

最上面的是old hash 表,其中的Hash表的 size = 2, 所以 key = 3, 7, 5,在 mod 2以后碰撞发生在 table[1]

接下来的三个步骤是 Hash表 resize 到4,并将所有的 重新rehash到新 Hash 表的过程

如图所示:

1.2 多线程 rehash 详细演示

为了思路更清晰,我们只将关键代码展示出来

  1. while(null != e) {
  2. Entry<K,V> next = e.next;
  3. e.next = newTable[i];
  4. newTable[i] = e;
  5. e = next;
  6. }
复制代码
Entry next = e.next; ——因为是单链表,如果要转移头指针,一定要保存下一个结点,不然转移后链表就丢了 e.next = newTable ; ——e 要插入到链表的头部,所以要先用 e.next 指向新的 Hash 表第一个元素(为什么不加到新链表最后?因为复杂度是 O(N)) newTable = e; ——现在新 Hash 表的头指针仍然指向 e 没转移前的第一个元素,所以需要将新 Hash 表的头指针指向 e e = next ——转移 e 的下一个结点

假设这里有两个线程同时执行了 put() 操作,并进入了 transfer() 环节

  1. while(null != e) {
  2. Entry<K,V> next = e.next; //线程1执行到这里被调度挂起了
  3. e.next = newTable[i];
  4. newTable[i] = e;
  5. e = next;
  6. }
复制代码

那么现在的状态为:

从上面的图我们可以看到,因为线程1的 e 指向了 key(3),而 next 指向了 key(7),在线程2 rehash 后,就指向了线程2 rehash 后的链表。

然后线程1被唤醒了:北京整容医院http://www.bj-swjtu.com/

执行 e.next = newTable  ,于是 key(3)的 next 指向了线程1的新 Hash 表,因为新 Hash 表为空,所以 e.next = null , 执行 newTable= e ,所以线程1的新 Hash 表第一个元素指向了线程2新 Hash 表的 key(3)。好了,e 处理完毕。 执行 e = next ,将 e 指向 next,所以新的 e 是 key(7)

然后该执行 key(3)的 next 节点 key(7)了:

现在的 e 节点是 key(7),首先执行 Entry next = e.next ,那么 next 就是 key(3)了 执行 e.next = newTable  ,于是key(7) 的 next 就成了 key(3) 执行 newTable = e ,那么线程1的新 Hash 表第一个元素变成了 key(7) 执行 e = next ,将 e 指向 next,所以新的 e 是 key(3)

这时候的状态图为:

然后又该执行 key(7)的 next 节点 key(3)了:

现在的 e 节点是 key(3),首先执行 Entry next = e.next ,那么 next 就是 null 执行 e.next = newTable  ,于是key(3) 的 next 就成了 key(7) 执行 newTable = e ,那么线程1的新 Hash 表第一个元素变成了 key(3) 执行 e = next ,将 e 指向 next,所以新的 e 是 key(7)

这时候的状态如图所示:北京艺星整形医院http://www.bj-swjtu.com/

很明显,环形链表出现了!!当然,现在还没有事情,因为下一个节点是 null,所以 transfer() 就完成了,等 put() 的其余过程搞定后,HashMap 的底层实现就是线程1的新 Hash 表了。

2. fail-fast

如果在使用迭代器的过程中有其他线程修改了map,那么将抛出ConcurrentModificationException,这就是所谓fail-fast策略。

这个异常意在提醒开发者及早意识到线程安全问题。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值