本博客日IP超过2000,PV 3000 左右,急需赞助商。
极客时间所有课程通过我的二维码购买后返现24元微信红包,请加博主新的微信号:xttblog2,之前的微信号好友位已满,备注:返现
受密码保护的文章请关注“业余草”公众号,回复关键字“0”获得密码
所有面试题(java、前端、数据库、springboot等)一网打尽,请关注文末小程序
腾讯云】1核2G5M轻量应用服务器50元首年,高性价比,助您轻松上云
我在前面的文章《Java 线程安全的3大核心:原子性、可见性、有序性》中已经说到了什么是线程安全!根据这篇文章,我们对照着 HashMap 来说说它为什么不是线程安全的?
前面我也强调过多次,回答是不是线程安全的请从:原子性、可见性、有序性(有的说顺序性,其实是一个意思)3 个方面来回答。参考着 3 个特性指标来说,HashMap 的 put、get、remove等没有一个是线程安全的。
下面我们通过一个例子来说说 HashMap 在并发多线程环境中使用是如何可能造成死循环问题的!
public class HashMapInfiniteLoopTest { // 业余草:www.xttblog.com private static HashMap<Integer,String> map = new HashMap<Integer,String>(2,0.75f); public static void main(String[] args) { map.put(5, "C"); new Thread("Thread1") { public void run() { map.put(7, "B"); System.out.println(map); }; }.start(); new Thread("Thread2") { public void run() { map.put(3, "A); System.out.println(map); }; }.start(); } }
需要说明的是,这个例子我是在 JDK 1.7 环境下测试的!
现在我对上面的代码,做一个简单的解读。
首先,初始化为一个长度为 2 的 HashMap,loadFactor=0.75,threshold=2*0.75=1,也就是说当 put 第二个 key 的时候,我们的 map 就需要进行 resize(扩容)。
为了快速说明线程安全问题,我们通过设置断点让线程1和线程2同时 debug 到 put 方法中调用的 transfer 方法的首行。
这时两个线程已经成功添加数据。放开 thread1 的断点至 transfer 方法的“Entry next = e.next;” 这一行;然后放开线程2的的断点,让线程2进行 resize。
我用一张图片来总结一下这个过程!
根据上图再结合源代码以及运行的案例得知,Thread1 的 e 指向了key(3),而 next 指向了 key(7),其在线程二 rehash 后,指向了线程二重组后的链表。
线程一被调度回来执行,先是执行 newTalbe[i] = e, 然后是 e = next,导致了 e 指向了 key(7),而下一次循环的 next = e.next 导致了 next 指向了 key(3)。
e.next = newTable[i] 导致 key(3).next 指向了 key(7)。这时的 key(7).next 已经指向了 key(3),于是环形链表就这样形成。
环形链表形成之后,如果后面有 get(1) 的操作,即取出这个元素的操作,就会发生 Infinite Loop(死循环)问题。
为什么会发生线程安全问题?
很简单,HashMap 的所有操作都没有保证线程安全的核心 3 要素,即:原子性、可见性、顺序性。所以它发生线程安全问题,我一点也不惊讶!
上面这个例子很简单,一点也不复杂,如果你看不懂,建议你操作我的操作,对照说明,一步一步调试。
最后,我在留一个问题,HashMap 初始化后,在没有任何元素的情况下,remove(null) 会报错吗?为什么?
最后,欢迎关注我的个人微信公众号:业余草(yyucao)!可加作者微信号:xttblog2。备注:“1”,添加博主微信拉你进微信群。备注错误不会同意好友申请。再次感谢您的关注!后续有精彩内容会第一时间发给您!原创文章投稿请发送至532009913@qq.com邮箱。商务合作也可添加作者微信进行联系!
本文原文出处:业余草: » 从 HashMap 的扩容机制来说它为什么不是线程安全的!