本课时我们主要讲解为什么 HashMap 是线程不安全的?而对于 HashMap,相信你一定并不陌生,HashMap 是我们平时工作和学习中用得非常非常多的一个容器,也是 Map 最主要的实现类之一,但是它自身并不具备线程安全的特点,可以从多种情况中体现出来,下面我们就对此进行具体的分析。

源码分析

第一步,我们来看一下 HashMap 中 put 方法的源码:

public V put(K key, V value) {if (key == null)return putForNullKey(value);int hash = hash(key.hashCode());int i = indexFor(hash, table.length);for (Entry<K,V> e = table[i]; e != null; e = e.next) {Object k;if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {V oldValue = e.value;e.value = value;e.recordAccess(this);return oldValue;}} //modCount++ 是一个复合操作modCount++;addEntry(hash, key, value, i);return null;
}

在 HashMap 的 put() 方法中,可以看出里面进行了很多操作,那么在这里,我们把目光聚焦到标记出来的 modCount++ 这一行代码中,相信有经验的小伙伴一定发现了,这相当于是典型的“i++”操作,正是我们在 06 课时讲过的线程不安全的“运行结果错误”的情况。从表面上看 i++ 只是一行代码,但实际上它并不是一个原子操作,它的执行步骤主要分为三步,而且在每步操作之间都有可能被打断。

  • 第一个步骤是读取;
  • 第二个步骤是增加;
  • 第三个步骤是保存。

那么我们接下来具体看一下如何发生的线程不安全问题。

我们根据箭头指向依次看,假设线程 1 首先拿到 i=1 的结果,然后进行 i+1 操作,但此时 i+1 的结果并没有保存下来,线程 1 就被切换走了,于是 CPU 开始执行线程 2,它所做的事情和线程 1 是一样的 i++ 操作,但此时我们想一下,它拿到的 i 是多少?实际上和线程 1 拿到的 i 的结果一样都是 1,为什么呢?因为线程 1 虽然对 i 进行了 +1 操作,但结果没有保存,所以线程 2 看不到修改后的结果。

然后假设等线程 2 对 i 进行 +1 操作后,又切换到线程 1,让线程 1 完成未完成的操作,即将 i + 1 的结果 2 保存下来,然后又切换到线程 2 完成 i = 2 的保存操作,虽然两个线程都执行了对 i 进行 +1 的操作,但结果却最终保存了 i = 2 的结果,而不是我们期望的 i = 3,这样就发生了线程安全问题,导致了数据结果错误,这也是最典型的线程安全问题。

所以,从源码的角度,或者说从理论上来讲,这完全足以证明 HashMap 是线程非安全的了。因为如果有多个线程同时调用 put() 方法的话,它很有可能会把 modCount 的值计算错(上述的源码分析针对的是 Java 7 版本的源码,而在 Java 8 版本的 HashMap 的 put 方法中会调用 putVal 方法,里面同样有 ++modCount 语句,所以原理是一样的)。

实验:扩容期间取出的值不准确

刚才我们分析了源码,你可能觉得不过瘾,下面我们就打开代码编辑器,用一个实验来证明 HashMap 是线程不安全的。

为什么说 HashMap 不是线程安全的呢?我们先来讲解下原理。HashMap 本身默认的容量不是很大,如果不停地往 map 中添加新的数据,它便会在合适的时机进行扩容。而在扩容期间,它会新建一个新的空数组,并且用旧的项填充到这个新的数组中去。那么,在这个填充的过程中,如果有线程获取值,很可能会取到 null 值,而不是我们所希望的、原来添加的值。所以我们程序就想演示这种情景,我们来看一下这段代码:

public class HashMapNotSafe {public static void main(String[] args) {final Map<Integer, String> map = new HashMap<>();final Integer targetKey = 0b1111_1111_1111_1111; // 65 535final String targetValue = "v";map.put(targetKey, targetValue);new Thread(() -> {IntStream.range(0, targetKey).forEach(key -> map.put(key, "someValue"));}).start();while (true) {if (null == map.get(targetKey)) {throw new RuntimeException("HashMap is not thread safe.");}}}
}

代码中首先建立了一个 HashMap,并且定义了 key 和 value, key 的值是一个二进制的 1111_1111_1111_1111,对应的十进制是 65535。之所以选取这样的值,就是为了让它在扩容往回填充数据的时候,尽量不要填充得太快,比便于我们能捕捉到错误的发生。而对应的 value 是无所谓的,我们随意选取了一个非 null 的 “v” 来表示它,并且把这个值放到了 map 中。

接下来,我们就用一个新的线程不停地往我们的 map 中去填入新的数据,我们先来看是怎么填入的。首先它用了一个 IntStream,这个 range 是从 0 到之前所讲过的 65535,这个 range 是一个左闭右开的区间,所以会从 0、1、2、3……一直往上加,并且每一次加的时候,这个 0、1、2、3、4 都会作为 key 被放到 map 中去。而它的 value 是统一的,都是 “someValue”,因为 value 不是我们所关心的。

然后,我们就会把这个线程启动起来,随后就进入一个 while 循环,这个 while 循环是关键,在 while 循环中我们会不停地检测之前放入的 key 所对应的 value 还是不是我们所期望的字符串 “v”。我们在 while 循环中会不停地从 map 中取 key 对应的值。如果 HashMap 是线程安全的,那么无论怎样它所取到的值都应该是我们最开始放入的字符串 “v”,可是如果取出来是一个 null,就会满足这个 if 条件并且随即抛出一个异常,因为如果取出 null 就证明它所取出来的值和我们一开始放入的值是不一致的,也就证明了它是线程不安全的,所以在此我们要抛出一个 RuntimeException 提示我们。

下面就让我们运行这个程序来看一看是否会抛出这个异常。一旦抛出就代表它是线程不安全的,这段代码的运行结果:

Exception in thread "main" java.lang.RuntimeException: HashMap is not thread safe.
at lesson29.HashMapNotSafe.main(HashMapNotSafe.java:25)

很明显,很快这个程序就抛出了我们所希望看到的 RuntimeException,并且我们把它描述为:HashMap is not thread safe,一旦它能进入到这个 if 语句,就已经证明它所取出来的值是 null,而不是我们期望的字符串 “v”。

通过以上这个例子,我们也证明了HashMap 是线程非安全的。

除了刚才的例子之外,还有很多种线程不安全的情况,例如:

同时 put 碰撞导致数据丢失

比如,有多个线程同时使用 put 来添加元素,而且恰好两个 put 的 key 是一样的,它们发生了碰撞,也就是根据 hash 值计算出来的 bucket 位置一样,并且两个线程又同时判断该位置是空的,可以写入,所以这两个线程的两个不同的 value 便会添加到数组的同一个位置,这样最终就只会保留一个数据,丢失一个数据。

可见性问题无法保证

我们再从可见性的角度去考虑一下。可见性也是线程安全的一部分,如果某一个数据结构声称自己是线程安全的,那么它同样需要保证可见性,也就是说,当一个线程操作这个容器的时候,该操作需要对另外的线程都可见,也就是其他线程都能感知到本次操作。可是 HashMap 对此是做不到的,如果线程 1 给某个 key 放入了一个新值,那么线程 2 在获取对应的 key 的值的时候,它的可见性是无法保证的,也就是说线程 2 可能可以看到这一次的更改,但也有可能看不到。所以从可见性的角度出发,HashMap 同样是线程非安全的。

死循环造成 CPU 100%

下面我们再举一个死循环造成 CPU 100% 的例子。HashMap 有可能会发生死循环并且造成 CPU 100% ,这种情况发生最主要的原因就是在扩容的时候,也就是内部新建新的 HashMap 的时候,扩容的逻辑会反转散列桶中的节点顺序,当有多个线程同时进行扩容的时候,由于 HashMap 并非线程安全的,所以如果两个线程同时反转的话,便可能形成一个循环,并且这种循环是链表的循环,相当于 A 节点指向 B 节点,B 节点又指回到 A 节点,这样一来,在下一次想要获取该 key 所对应的 value 的时候,便会在遍历链表的时候发生永远无法遍历结束的情况,也就发生 CPU 100% 的情况。

所以综上所述,HashMap 是线程不安全的,在多线程使用场景中如果需要使用 Map,应该尽量避免使用线程不安全的 HashMap。同时,虽然 Collections.synchronizedMap(new HashMap()) 是线程安全的,但是效率低下,因为内部用了很多的 synchronized,多个线程不能同时操作。推荐使用线程安全同时性能比较好的 ConcurrentHashMap。关于 ConcurrentHashMap 我们会在下一个课时中介绍。

Java并发编程78讲--29 第29讲:HahMap 为什么是线程不安全的?相关推荐

  1. Java并发编程原理与实战六:主线程等待子线程解决方案

    Java并发编程原理与实战六:主线程等待子线程解决方案 参考文章: (1)Java并发编程原理与实战六:主线程等待子线程解决方案 (2)https://www.cnblogs.com/pony1223 ...

  2. Java并发编程:从源码分析几道必问线程池的面试题?

    引言 上一篇文章我们有介绍过线程池的一个基本执行流程<[Java并发编程]面试必备之线程池>以及它的7个核心参数,以及每个参数的作用.以及如何去使用线程池 还留了几个小问题..建议看这篇文 ...

  3. Java 并发编程(二)对象的发布逸出和线程封闭

    对象的发布与逸出 "发布(Publish)"一个对象是指使对象能够在当前作用域之外的代码中使用.可以通过 公有静态变量,非私有方法,构造方法内隐含引用 三种方式. 如果对象构造完成 ...

  4. Java并发编程78讲--27 第27讲:什么是自旋锁?自旋的好处和后果是什么呢?

    在本课时我们主要讲解什么是自旋锁?以及使用自旋锁的好处和后果分别是什么呢? 什么是自旋 首先,我们了解什么叫自旋?"自旋"可以理解为"自我旋转",这里的&quo ...

  5. Java并发编程—Thread类的start()方法是如何启动一个线程的?

    目录 一:Java线程介绍 二:Java线程入口分析 三:Java线程的创建 四:总结 周末抽了点时间,研究了下HotSpot是如何创建Java线程的,顺便总结一下.文中引用的源码里删除很多细节,只保 ...

  6. Java并发编程|第二篇:线程生命周期

    文章目录 系列文章 1.线程的状态 2.线程生命周期 3.状态测试代码 4.线程终止 4.1 线程执行完成 4.2 interrupt 5.线程复位 5.1interrupted 5.2抛出异常 6. ...

  7. 【基础】Java 并发编程(上)

    Java 并发编程(上) JUC 概述 什么是 JUC? 线程与进程 并发与并行 线程基础概述 线程的状态 线程的活性故障 死锁的必要条件 避免死锁的方法 线程锁死的分类 公平调度与非公平调度 多线程 ...

  8. Java并发编程:进程、线程、并行与并发

    一谈到Java并发编程,我们一般就会联想起进程.线程.并行.并发等等概念.那么这些概念都代表什么呢?进程与线程有什么关系?并发与并行又是什么关系呢? 01 进程与线程 进程是指程序的一次动态执行过程, ...

  9. Java并发编程71道面试题及答案

    Java并发编程71道面试题及答案 1.在java中守护线程和本地线程区别? java中的线程分为两种:守护线程(Daemon)和用户线程(User). 任何线程都可以设置为守护线程和用户线程,通过方 ...

最新文章

  1. java reactor框架_Java反应式框架Reactor中的Mono和Flux
  2. [转] Java中的static关键字解析
  3. python aiohttp_aiohttp
  4. 当我们在谈论HTTP缓存时我们在谈论什么
  5. 数据访问模式二:数据集和数据适配器(传统的数据访问模式)
  6. 在运行时访问工件的Maven和SCM版本
  7. Java LocalDate类| 带示例的compareTo()方法
  8. Python:集合、三元运算符
  9. 使用getApplication()作为上下文的对话框抛出“无法添加窗口-令牌null不适用于应用程序”
  10. 大学matlab教程,大学数学MATLAB应用教程
  11. MatConvNet训练自己的网络
  12. 鸿蒙开放beta版有什么用,鸿蒙2.0beta活动有什么内容 鸿蒙2.0 Beta手机版什么时候发布...
  13. linux扩展模式触摸屏,Ubuntu14.04下使用触摸屏以及笔记本扩展触摸屏设置方法
  14. s8 android调用相机,android-扎根的Galaxy S8上的设备所有者
  15. ppt矩形里面的图片怎么放大缩小_ppt 怎么让图片放大完成以后再缩小到原来的大小和位子!!!!...
  16. Fastqc安装运行(jdk安装)
  17. 短信服务平台怎么选?
  18. GIF表情包怎么制作 QQ动态图如何玩
  19. SLCP认证辅导,SLCP认证产品应覆盖不同的产品类别
  20. 一、Python-劳务报酬计算器(新手练习)

热门文章

  1. 2018.09.06 烽火传递(单调队列优化dp)
  2. jQuery输入框回车添加标签特效
  3. 二分类变量相关性分析spss_SPSS学习笔记13:处理分类变量的利器,对应分析
  4. java byte数组转long_Java中byte、byte数组与int、long的转换详解
  5. yourenduwanglai的鬼话连篇(八)
  6. Swift5.x - 中文文档
  7. 高通 NVIDIA物联网芯片引入AI功能抢占市场高地
  8. ibm 蓝色之路 java_IBM蓝色之路一面
  9. Make PGP messages and signatures more interesting
  10. Excel2016打开文件需要两次双击才能打开-注册表解决