这两天写爬虫帮组里收集网上数据做训练,需要进一步对收集到的json数据做数据清洗,结果就用到了多线程下的哈希表数据结构,猛地回想起自己看《Java并发编程的艺术》框架篇的时候,在ConcurrentHashMap的章节看到过使用HashMap是线程不安全的,HashTable虽然安全但效率很低,推荐使用ConcurrentHashMap巴拉巴拉,突然有了兴趣来查阅一下各自的源码,看看具体区别在哪里呢?HashMao为什么线程不安全?顺带记录下来,还是那句话,好记性不如烂笔头


我们知道的Java中的哈希表数据结构有下面三种

  • HashMap
  • HashTable
  • ConcurrentHashMap

下面就依次来看看它们是如何保证并发时可靠的,各自有什么优缺点

HashMap

首先是大家都很熟悉的哈希表:HashMap,刷算法题必备哈希表数据结构。它的存储结构如下图所示

很好看懂的一个图,简单来说就是HashMap采用的是拉链法处理哈希冲突。所谓哈希冲突就是由于哈希表根据哈希值索引目标节点来对随机存取获得O(1)的时间复杂度,那么每个哈希值当然只能站一个节点,如果存在多个节点计算出的哈希值一致就发生了哈希冲突,此时一般有三种思路:

  • 拉链法:在一个哈希值上设置一个数据集结构,也就是一个哈希值代表一个数据集,我们对数据集的随机存取获得O(1)时间复杂度,对数据集内获取目标Key节点获得O(m)时间复杂度,如果哈希值的数量远多于数据集内节点的数量,那么我们近似取到O(1)时间复杂度
  • 开放定址法:一旦碰到哈希冲突就顺延后来的节点的哈希值,比如节点A取哈希为1,而哈希值1,2,3上都已经有节点在了,那么我们根据顺延规则取4作为该节点的真实存储位置,这种方案一般表现比较糟糕
  • 再哈希法:同时构造多个不同的哈希函数,等发生哈希冲突时就使用第二个、第三个,第四个等等等等的其他的哈希函数计算地址,直到不发生冲突为止。虽然不易发生聚集,但是大大增加了计算时间

这里我们常用的三种哈希表结构全部是采用的拉链法,这是一种认可度较高的解决方案,那么拉链法就要求我们每个哈希值都独立设置一个链表来存储哈希冲突的节点。那么我们关于多线程安全的问题自然也就来自于此。

总的来说,HashMap在多线程时视使用的Java版本有以下三大问题

  • 数据覆盖(一直存在)
  • 死循环(JDK1.7前存在)
  • 数据丢失(JDK1.7前存在)

    我们一个一个来看

数据覆盖

顾名思义,两个线程同时往里面放数据,但是其中一个数据放丢了,这个问题是根本问题,目前的HashMap依然有这个问题,出问题的原因也很简单,HashMap本来就没做多线程适配当然出问题,但是原理还是值得一看。

  1. // 代码截取自HashMap.java,方法final V putVal()中
  2. for (int binCount = 0; ; ++binCount) {
  3. // 根据下面代码,我们看出其实插入新节点就是反复探测目前节点的next指针是否为空,若为空则在该指针上插入新节点
  4. if ((e = p.next) == null) {
  5. p.next = newNode(hash, key, value, null);
  6. if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
  7. treeifyBin(tab, hash);
  8. break;
  9. }
  10. if (e.hash == hash &&
  11. ((k = e.key) == key || (key != null && key.equals(k))))
  12. break;
  13. p = e;
  14. }

看完以后我们发现,其实就是很简单的探测链表尾部的方法,看该节点next指针是否为null,若为null说明是尾节点,在其后插入新节点,那么数据覆盖的真相也就很简单了:A,B两个线程同时向同一个哈希值的链表发起插入,A探测到C节点的next为空然后时间片用完被换下,此时B也探测到C的next为空并完成了插入,等到A再次换入时间片,完成插入,最终,A,B运行结束,但B插入的新节点就这样消失了。这就是数据覆盖问题。

死循环与数据丢失

其实这两个问题的核心都来自JDK1.7前,HashMap的扩容操作(扩容采用头插法插入)会重新定位每个桶的下标,并采用头插法将元素迁移到新数组中。而头插法会将链表的顺序翻转,这也是造成死循环和数据丢失的关键。

  1. void transfer(Entry[] newTable, boolean rehash) {
  2. int newCapacity = newTable.length;
  3. // 采用头插法
  4. for (Entry<K,V> e : table) {
  5. while(null != e) {
  6. Entry<K,V> next = e.next;
  7. if (rehash) {
  8. e.hash = null == e.key ? 0 : hash(e.key);
  9. }
  10. int i = indexFor(e.hash, newCapacity);
  11. e.next = newTable[i];
  12. newTable[i] = e;
  13. e = next;
  14. }
  15. }
  16. }

怎么一回事呢?

现在假设线程启动,有线程A和线程B都准备对HashMap进行扩容操作, 此时A和B指向的都是链表的头节点NodeA,而A和B的下一个节点的指针即head.next和head.next都指向NodeB节点。

那么,开始扩容,这时候,假设线程B的时间片用完,被换下CPU,而线程A开始执行扩容操作,一直到线程A扩容完成后,线程B才被唤醒。

此时因为HashMap扩容采用的是头插法,线程A执行之后,链表中的节点顺序已经倒转,本来NodeA->NodeB,现在变成了NodeB->NodeA。但就绪态的线程B对于发生的一切都不清楚,所以它指向的节点引用依然没变。那么一旦B被换上CPU,重复一次刚刚A做过的事情,就会导致NodeA和NodeB的next指针相互指向,导致死循环和数据丢失。

不过JDK1.8以后,HashMap的哈希值扩容改为了尾插法扩容,就不会再出现这些问题了。


HashTable

效率很差的一个类,根据我自己的周边统计学,我的感觉是这个玩意根本没人用,用它就类似于如果你给线上项目的MySQL突然整出个表级锁一样,等你的只能是一通臭骂!为什么呢?看下面

  1. // 你就看这个synchronized关键字就可以了,不用往下看了
  2. public synchronized V put(K key, V value) {
  3. // Make sure the value is not null
  4. if (value == null) {
  5. throw new NullPointerException();
  6. }
  7. // Makes sure the key is not already in the hashtable.
  8. Entry<?,?> tab[] = table;
  9. int hash = key.hashCode();
  10. int index = (hash & 0x7FFFFFFF) % tab.length;
  11. @SuppressWarnings("unchecked")
  12. Entry<K,V> entry = (Entry<K,V>)tab[index];
  13. for(; entry != null ; entry = entry.next) {
  14. if ((entry.hash == hash) && entry.key.equals(key)) {
  15. V old = entry.value;
  16. entry.value = value;
  17. return old;
  18. }
  19. }
  20. addEntry(hash, key, value, index);
  21. return null;
  22. }

很明显了。HashTable采用了Synchronized关键字来保证线程安全。

我们知道Synchronized关键字的底层原理是给对象头上的MarkWord的内容做改动从而将该对象当作互斥变量使用,也就是说,这把锁是对象级别的。

问题就在于我本来哈希冲突只是一个哈希值上的冲突,而你的解决方案是锁住整个哈希表,这会不会有点太过分了?可以说表级锁的比喻是很贴切了。

不推荐使用,效率很低。


ConcurrentHashMap

《Java并发编程的艺术》里面提到的第一个并发结构,它的思路就是在HashTable表级锁的基础上把它改为行级锁,什么意思呢?放源码

  1. // 截取自ConcurrentHashMap.java,final V putVal()方法
  2. // 注意下面这句,f 是我们需要的哈希值对应的首节点
  3. else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
  4. if (casTabAt(tab, i, null, new Node<K,V>(hash, key, value)))
  5. break; // no lock when adding to empty bin
  6. }
  7. else if ((fh = f.hash) == MOVED)
  8. tab = helpTransfer(tab, f);
  9. else if (onlyIfAbsent // check first node without acquiring lock
  10. && fh == hash
  11. && ((fk = f.key) == key || (fk != null && key.equals(fk)))
  12. && (fv = f.val) != null)
  13. return fv;
  14. else {
  15. V oldVal = null;
  16. // 来到这里,上面的都不用看,主要是排查初始化情况,这里synchronized(f)就解释清楚了我们的ConcurrentHashMap的方法
  17. synchronized (f) {
  18. if (tabAt(tab, i) == f) {
  19. if (fh >= 0) {
  20. binCount = 1;
  21. for (Node<K,V> e = f;; ++binCount) {
  22. K ek;
  23. if (e.hash == hash &&
  24. ((ek = e.key) == key ||
  25. (ek != null && key.equals(ek)))) {
  26. oldVal = e.val;
  27. if (!onlyIfAbsent)
  28. e.val = value;
  29. break;
  30. }
  31. Node<K,V> pred = e;
  32. if ((e = e.next) == null) {
  33. pred.next = new Node<K,V>(hash, key, value);
  34. break;
  35. }
  36. }
  37. }

很明显,ConcurrentHashMap同样是通过Synchronized关键字来实现线程安全的,只不过这把锁从原来的表级锁,变为了以首节点为对象的行级锁,当我们并发的对ConcurrentHashMap操作时,锁只会锁住某一个哈希值,而不会锁住整个表,保证了我们的哈希表在高并发场景下的效率。

总结

HashMap只适用于非并发情况下,ConcurrentHashMap适用于并发情况下,而HashTable则不推荐使用

HashMap为何线程不安全?HashMap,HashTable,ConcurrentHashMap对比的更多相关文章

  1. [Java集合] 彻底搞懂HashMap,HashTable,ConcurrentHashMap之关联.

    注: 今天看到的一篇讲hashMap,hashTable,concurrentHashMap很透彻的一篇文章, 感谢原作者的分享. 原文地址: http://blog.csdn.net/zhanger ...

  2. Java集合——HashMap,HashTable,ConcurrentHashMap区别

    Map:“键值”对映射的抽象接口.该映射不包括重复的键,一个键对应一个值. SortedMap:有序的键值对接口,继承Map接口. NavigableMap:继承SortedMap,具有了针对给定搜索 ...

  3. 彻底搞懂HashMap,HashTable,ConcurrentHashMap之关联.

    注: 今天看到的一篇讲hashMap,hashTable,concurrentHashMap很透彻的一篇文章, 感谢原作者的分享.  原文地址: http://blog.csdn.net/zhange ...

  4. 浅谈HashMap与线程安全 (JDK1.8)

    HashMap是Java程序员使用频率最高的用于映射(键值对)处理的数据类型.HashMap 继承自 AbstractMap 是基于哈希表的 Map 接口的实现,以 Key-Value 的形式存在,即 ...

  5. hashmap,hashTable concurrentHashMap 是否为线程安全,区别,如何实现的

    线程安全类 在集合框架中,有些类是线程安全的,这些都是jdk1.1中的出现的.在jdk1.2之后,就出现许许多多非线程安全的类. 下面是这些线程安全的同步的类: vector:就比arraylist多 ...

  6. 为什么HashMap线程不安全,Hashtable和ConcurrentHashMap线程安全

    HashMap源码 public V put(K key, V value) { return putVal(hash(key), key, value, false, true); } final ...

  7. HashMap,HashTable,concurrentHashMap,LinkedHashMap 区别

    HashMap 不是线程安全的 HashTable,concurrentHashMap 是线程安全 HashTable 底层是所有方法都加有锁(synchronized) 所以操作起来效率会低 con ...

  8. 非线程安全的HashMap 和 线程安全的ConcurrentHashMap

    在平时开发中,我们经常采用HashMap来作为本地缓存的一种实现方式,将一些如系统变量等数据量比较少的参数保存在HashMap中,并将其作为单例类的一个属性.在系统运行中,使用到这些缓存数据,都可以直 ...

  9. java基础解析系列(五)---HashMap并发下的问题以及HashTable和CurrentHashMap的区别

    java基础解析系列(五)---HashMap并发下的问题以及HashTable和CurrentHashMap的区别 目录 java基础解析系列(一)---String.StringBuffer.St ...

  10. Java 非线程安全的HashMap如何在多线程中使用

    Java 非线程安全的HashMap如何在多线程中使用 HashMap 是非线程安全的.在多线程条件下,容易导致死循环,具体表现为CPU使用率100%.因此多线程环境下保证 HashMap 的线程安全 ...

随机推荐

  1. 前端必读3.0:如何在 Angular 中使用SpreadJS实现导入和导出 Excel 文件

    在之前的文章中,我们为大家分别详细介绍了在JavaScript.React中使用SpreadJS导入和导出Excel文件的方法,作为带给广大前端开发者的"三部曲",本文我们将为大家 ...

  2. kubernetes 调度器

    调度器 kube-scheduler 是 kubernetes 的核心组件之一,主要负责整个集群资源的调度功能,根据特定的调度算法和策略,将 Pod 调度到最优的工作节点上面去,从而更加合理.更加充分 ...

  3. parted创建磁盘分区并创建LVM(Linux合并多块大于2T的磁盘并合并到一个分区)

    文章转载自:https://blog.csdn.net/likemebee/article/details/85630808

  4. Elasticsearch:定制分词器(analyzer)及相关性

    转载自:https://elasticstack.blog.csdn.net/article/details/114278163 在许多的情况下,我们使用现有的分词器已经足够满足我们许多的业务需求,但 ...

  5. 关于windows-server-下MySQL Community版本的的安装与配置

    在公司电脑或者服务器上安装软件,都是有要求的,要么购买license-(这个需要申请,难度较大),要么安装免费开源的软件 笔者最近想要安装mysql服务环境,用于数据存储及开发一些功能程序需要连接数据 ...

  6. Linx__Ubuntu_APT

    apt介绍 apt是Advanced Packaging Tool的简称. 在Ubuntu下,我们可以使用apt命令进行软件包的更新,安装,删除,清理等 类似于Windows的软件管理工具. 就是Ce ...

  7. 【C++】从零开始的CS:GO逆向分析3——写出一个透视

    [C++]从零开始的CS:GO逆向分析3--写出一个透视 本篇内容包括: 1. 透视实现的方法介绍 2. 通过进程名获取进程id和进程句柄 3. 通过进程id获取进程中的模块信息(模块大小,模块地址, ...

  8. SpringMVC入手项目注解版

    SpringMVC入手项目注解版 1.创建Maven项目在pom.xml文件引入相关的依赖 <dependencies> <dependency> <groupId> ...

  9. MPI实现并行奇偶排序

    奇偶排序 odd-even-sort, using MPI 代码在 https://github.com/thkkk/odd-even-sort 使用 MPI 实现奇偶排序算法, 并且 MPI 进程 ...

  10. 网页头部的声明应该是用 lang="zh" 还是 lang="zh-CN"?

    网页头部的声明应该是用 lang="zh" 还是 lang="zh-CN"? 遇到问题 不知道大家有没有留意到一个问题,就是使用 VsCode 新建的 html ...