这两天写爬虫帮组里收集网上数据做训练,需要进一步对收集到的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本来就没做多线程适配当然出问题,但是原理还是值得一看。

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

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

死循环与数据丢失

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

void transfer(Entry[] newTable, boolean rehash) {
int newCapacity = newTable.length;
// 采用头插法
for (Entry<K,V> e : table) {
while(null != e) {
Entry<K,V> next = e.next;
if (rehash) {
e.hash = null == e.key ? 0 : hash(e.key);
}
int i = indexFor(e.hash, newCapacity);
e.next = newTable[i];
newTable[i] = e;
e = next;
}
}
}

怎么一回事呢?

现在假设线程启动,有线程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突然整出个表级锁一样,等你的只能是一通臭骂!为什么呢?看下面

// 你就看这个synchronized关键字就可以了,不用往下看了
public synchronized V put(K key, V value) {
// Make sure the value is not null
if (value == null) {
throw new NullPointerException();
} // Makes sure the key is not already in the hashtable.
Entry<?,?> tab[] = table;
int hash = key.hashCode();
int index = (hash & 0x7FFFFFFF) % tab.length;
@SuppressWarnings("unchecked")
Entry<K,V> entry = (Entry<K,V>)tab[index];
for(; entry != null ; entry = entry.next) {
if ((entry.hash == hash) && entry.key.equals(key)) {
V old = entry.value;
entry.value = value;
return old;
}
} addEntry(hash, key, value, index);
return null;
}

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

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

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

不推荐使用,效率很低。


ConcurrentHashMap

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

// 截取自ConcurrentHashMap.java,final V putVal()方法
// 注意下面这句,f 是我们需要的哈希值对应的首节点
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
if (casTabAt(tab, i, null, new Node<K,V>(hash, key, value)))
break; // no lock when adding to empty bin
}
else if ((fh = f.hash) == MOVED)
tab = helpTransfer(tab, f);
else if (onlyIfAbsent // check first node without acquiring lock
&& fh == hash
&& ((fk = f.key) == key || (fk != null && key.equals(fk)))
&& (fv = f.val) != null)
return fv;
else {
V oldVal = null;
// 来到这里,上面的都不用看,主要是排查初始化情况,这里synchronized(f)就解释清楚了我们的ConcurrentHashMap的方法
synchronized (f) {
if (tabAt(tab, i) == f) {
if (fh >= 0) {
binCount = 1;
for (Node<K,V> e = f;; ++binCount) {
K ek;
if (e.hash == hash &&
((ek = e.key) == key ||
(ek != null && key.equals(ek)))) {
oldVal = e.val;
if (!onlyIfAbsent)
e.val = value;
break;
}
Node<K,V> pred = e;
if ((e = e.next) == null) {
pred.next = new Node<K,V>(hash, key, value);
break;
}
}
}

很明显,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. 5.第四篇 Etcd存储组件高可用部署

    文章转载自:https://mp.weixin.qq.com/s?__biz=MzI1MDgwNzQ1MQ==&mid=2247483792&idx=1&sn=b991443c ...

  2. 使用国内镜像源安装kubelet kubeadm kubectl

    由于官网未开放同步方式, 可能会有索引gpg检查失败的情况, 这时请用 yum install -y --nogpgcheck kubelet kubeadm kubectl 安装 Debian / ...

  3. k8s控制器和Pod Template的关系

    Pod 本身并不能自愈(self-healing).如果一个 Pod 所在的 Node (节点)出现故障,或者调度程序自身出现故障,Pod 将被删除:同理,当因为节点资源不够或节点维护而驱逐 Pod ...

  4. 研发效能之技术治理&技术治理架构师

    最近很多公司专门设置了一个职位叫「技术治理架构师」,主要负责公司技术治理相关事宜.这是个非常有意思的职位.技术治理的活,之前我们也是做的,只是没有提的这么明确,一般都是研发效能团队.PMO.架构团队. ...

  5. 【C++】从零开始的CS:GO逆向分析2——配置GLFW+IMGUI环境并创建透明窗口

    [C++]从零开始的CS:GO逆向分析2--配置GLFW+IMGUI环境并创建透明窗口   使用的环境:Visual Studio 2017,创建一个控制台程序作为工程文件 1.配置glfw 在git ...

  6. 我公司是属于生产制造业,最近考虑实施ERP,生产制造业的ERP那家比较好?

    直接告诉你用哪家ERP,那我就太不负责任了,不同企业的规模选用不同的系统,匹配很重要!比如你大型企业,业务管理都比较标准规范,变化性也不大,不差钱预算没问题(千万元起步),你可以考虑下头部厂商.但如果 ...

  7. 换工作?试试远程工作「GitHub 热点速览 v.22.40」

    近日,潜在某个技术交流群的我发现即将毕业的小伙伴在焦虑实习.校招,刚好本周 GitHub 热榜有个远程工作项目.不妨大家换个思路,"走"出去也许有更多的机会.当然,除了全球的远程工 ...

  8. SpringBoot(五) - Java8 新特性

    1.Lambda表达式 Lambda 是一个匿名函数,我们可以把 Lambda 表达式理解为是一段可以传递的代码(将代码像数据一样进行传递).使用它可以写出更简洁.更灵活的代码.作为一种更紧凑的代码风 ...

  9. sql语法巧用之not取反

    数据库的重要性和通用性都不用说了,什么sql的通用性,sql优化之类的也不必说了,咱们今天来聊聊另一个有意思的话题:如何取一个筛选的反面案例. 1. 举几个正反案例的例子 为了让大家理解我们的假设场景 ...

  10. html+css 面试题总结附答案

    行内元素有哪些? 块级元素有哪些? 块级元素:div p h1 ul li form table行内元素: a b br i span input select laber strong em img ...