JDK1.7和JDK1.8中HashMap是线程不安全的,并发容器ConcurrentHashMap模型

一、HashMap是线程不安全的

前言

只要是对于集合有一定了解的一定都知道HashMap是线程不安全的,我们应该使用ConcurrentHashMap。但是为什么HashMap是线程不安全的呢,之前面试的时候也遇到到这样的问题,但是当时只停留在***知道是***的层面上,并没有深入理解***为什么是***。于是今天重温一个HashMap线程不安全的这个问题。

首先需要强调一点,HashMap的线程不安全体现在会造成死循环、数据丢失、数据覆盖这些问题。其中死循环和数据丢失是在JDK1.7中出现的问题,在JDK1.8中已经得到解决,然而1.8中仍会有数据覆盖这样的问题。

扩容引发的线程不安全

HashMap的线程不安全主要是发生在扩容函数中,即根源是在transfer函数中,JDK1.7中HashMaptransfer函数如下:

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;}}}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

这段代码是HashMap的扩容操作,重新定位每个桶的下标,并采用头插法将元素迁移到新数组中。头插法会将链表的顺序翻转,这也是形成死循环的关键点。理解了头插法后再继续往下看是如何造成死循环以及数据丢失的。

扩容造成死循环和数据丢失的分析过程

假设现在有两个线程A、B同时对下面这个HashMap进行扩容操作:

正常扩容后的结果是下面这样的:

但是当线程A执行到上面transfer函数的第11行代码时,CPU时间片耗尽,线程A被挂起。即如下图中位置所示:

此时线程A中:e=3、next=7、e.next=null

当线程A的时间片耗尽后,CPU开始执行线程B,并在线程B中成功的完成了数据迁移

重点来了,根据Java内存模式可知,线程B执行完数据迁移后,此时主内存中newTabletable都是最新的,也就是说:7.next=3、3.next=null。

随后线程A获得CPU时间片继续执行newTable[i] = e,将3放入新数组对应的位置,执行完此轮循环后线程A的情况如下:

接着继续执行下一轮循环,此时e=7,从主内存中读取e.next时发现主内存中7.next=3,于是乎next=3,并将7采用头插法的方式放入新数组中,并继续执行完此轮循环,结果如下:

执行下一次循环可以发现,next=e.next=null,所以此轮循环将会是最后一轮循环。接下来当执行完e.next=newTable[i]即3.next=7后,3和7之间就相互连接了,当执行完newTable[i]=e后,3被头插法重新插入到链表中,执行结果如下图所示:

上面说了此时e.next=null即next=null,当执行完e=null后,将不会进行下一轮循环。到此线程A、B的扩容操作完成,很明显当线程A执行完后,HashMap中出现了环形结构,当在以后对该HashMap进行操作时会出现死循环。

并且从上图可以发现,元素5在扩容期间被莫名的丢失了,这就发生了数据丢失的问题。

JDK1.8中的线程不安全

根据上面JDK1.7出现的问题,在JDK1.8中已经得到了很好的解决,如果你去阅读1.8的源码会发现找不到transfer函数,因为JDK1.8直接在resize函数中完成了数据迁移。另外说一句,JDK1.8在进行元素插入时使用的是尾插法。

为什么说JDK1.8会出现数据覆盖的情况喃,我们来看一下下面这段JDK1.8中的put操作代码:

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict) {Node<K,V>[] tab; Node<K,V> p; int n, i;if ((tab = table) == null || (n = tab.length) == 0)n = (tab = resize()).length;if ((p = tab[i = (n - 1) & hash]) == null) // 如果没有hash碰撞则直接插入元素tab[i] = newNode(hash, key, value, null);else {Node<K,V> e; K k;if (p.hash == hash &&((k = p.key) == key || (key != null && key.equals(k))))e = p;else if (p instanceof TreeNode)e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);else {for (int binCount = 0; ; ++binCount) {if ((e = p.next) == null) {p.next = newNode(hash, key, value, null);if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1sttreeifyBin(tab, hash);break;}if (e.hash == hash &&((k = e.key) == key || (key != null && key.equals(k))))break;p = e;}}if (e != null) { // existing mapping for keyV oldValue = e.value;if (!onlyIfAbsent || oldValue == null)e.value = value;afterNodeAccess(e);return oldValue;}}++modCount;if (++size > threshold)resize();afterNodeInsertion(evict);return null;}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42

其中第六行代码是判断是否出现hash碰撞,假设两个线程A、B都在进行put操作,并且hash函数计算出的插入下标是相同的,当线程A执行完第六行代码后由于时间片耗尽导致被挂起,而线程B得到时间片后在该下标处插入了元素,完成了正常的插入,然后线程A获得时间片,由于之前已经进行了hash碰撞的判断,所有此时不会再进行判断,而是直接进行插入,这就导致了线程B插入的数据被线程A覆盖了,从而线程不安全。

除此之前,还有就是代码的第38行处有个++size,我们这样想,还是线程A、B,这两个线程同时进行put操作时,假设当前HashMap的zise大小为10,当线程A执行到第38行代码时,从主内存中获得size的值为10后准备进行+1操作,但是由于时间片耗尽只好让出CPU,线程B快乐的拿到CPU还是从主内存中拿到size的值10进行+1操作,完成了put操作并将size=11写回主内存,然后线程A再次拿到CPU并继续执行(此时size的值仍为10),当执行完put操作后,还是将size=11写回内存,此时,线程A、B都执行了一次put操作,但是size的值只增加了1,所有说还是由于数据覆盖又导致了线程不安全。

总结

HashMap的线程不安全主要体现在下面两个方面:
**1.在JDK1.7中,当并发执行扩容操作时会造成环形链和数据丢失的情况。**
**2.在JDK1.8中,在并发执行put操作时会发生数据覆盖的情况。**



二、并发容器ConcurrentHashMap——JDK1.7与JDK1.8区别

在Java常用的容器HashMap存在着线程不安全的问题,其中JDK1.7与JDK1.8的线程不安全会出现不同的情况:在多线程情况下,JDK1.7在HashMap在扩容时会造成环形;在JDK1.8中可能会发生数据覆盖。

1、JDK1.7下的ConcurrentHashMap

ConcurrentHashMap是由Segment数组结构和HashEntry数组结构组成。Segment实际继承自可重入锁(ReentrantLock),在ConcurrentHashMap里扮演锁的角色;HashEntry则用于存储键值对数据。一个ConcurrentHashMap里包含一个Segment数组,每个Segment里包含一个HashEntry数组,我们称之为table,每个HashEntry是一个链表结构的元素。

1.1 初始化

初始化有三个参数

initialCapacity:初始容量大小 ,默认16。

loadFactor, 扩容因子,默认0.75,当一个Segment存储的元素数量大于initialCapacity* loadFactor时,该Segment会进行一次扩容。

concurrencyLevel 并发度,默认16。并发度可以理解为程序运行时能够同时更新ConccurentHashMap且不产生锁竞争的最大线程数,实际上就是ConcurrentHashMap中的分段锁个数,即Segment[]的数组长度。如果并发度设置的过小,会带来严重的锁竞争问题;如果并发度设置的过大,原本位于同一个Segment内的访问会扩散到不同的Segment中,CPU cache命中率会下降,从而引起程序性能下降。

构造方法:

保证Segment数组的大小,一定为2的幂,例如用户设置并发度为17,则实际Segment数组大小则为32。

保证每个Segment中tabel数组的大小,一定为2的幂,初始化的三个参数取默认值时,table数组大小为2

初始化Segment数组,并实际只填充Segment数组的第0个元素

用于定位元素所在segment。segmentShift表示偏移位数,通过前面的int类型的位的描述我们可以得知,int类型的数字在变大的过程中,低位总是比高位先填满的,为保证元素在segment级别分布的尽量均匀,计算元素所在segment时,总是取hash值的高位进行计算。segmentMask作用就是为了利用位运算中取模的操作:a % (Math.pow(2,n)) 等价于 a&( Math.pow(2,n)-1)。

1.2 如何实现高并发下的线程安全

ConcurrentHashMap允许多个修改操作并发进行,其关键在于使用了锁分离技术。它使用了多个锁来控制对hash表的不同部分进行的修改。内部使用段(Segment)来表示这些不同的部分,每个段其实就是一个小的hash table,只要多个修改操作发生在不同的段上,它们就可以并发进行。

1.3 如何快速定位元素

对于某个元素而言,一定是放在某个segment元素的某个table元素中的。

定位segment:取得key的hashcode值进行一次再散列(通过Wang/Jenkins算法),拿到再散列值后,以再散列值的高位进行取模得到当前元素在哪个segment上。

定位table:同样是取得key的再散列值以后,用再散列值的全部和table的长度进行取模,得到当前元素在table的哪个元素上。

1.4 get方法

定位segment和定位table后,依次扫描这个table元素下的的链表,要么找到元素,要么返回null。

在高并发下的情况下如何保证取得的元素是最新的?

用于存储键值对数据的HashEntry,在设计上它的成员变量value等都是volatile类型的,这样就保证别的线程对value值的修改,get方法可以马上看到。

1.5 put方法

  • 首先定位segment,当这个segment在map初始化后,还为null,由ensureSegment方法负责填充这个segment。
  • 对Segment 加锁

  • 定位所在的table元素,并扫描table下的链表,找到时:

没有找到时:

 

1.6 扩容操作

假设原来table长度为4,那么元素在table中的分布是这样的:

Hash值

15

23

34

56

77

在table中下标

3  = 15%4

3 = 23 % 4

2 = 34%4

0 = 56%4

1 = 77 % 4

扩容后table长度变为8,那么元素在table中的分布变成:

Hash值

56

 

34

 

 

77

 

15,23

下标

0

1

2

3

4

5

6

7

可以看见 hash值为34和56的下标保持不变,而15,23,77的下标都是在原来下标的基础上+4即可,可以快速定位和减少重排次数。

1.7 弱一致性

get方法和containsKey方法都是通过对链表遍历判断是否存在key相同的节点以及获得该节点的value。但由于遍历过程中其他线程可能对链表结构做了调整,因此get和containsKey返回的可能是过时的数据,这一点是ConcurrentHashMap在弱一致性上的体现。

2、JDK1.8下的ConcurrentHashMap

2.1 与JDK1.7相比的主要变化

  • 取消了segment数组,直接用table数组保存数据,锁的粒度更小,减少并发冲突的概率。
  • 存储数据时采用了链表+红黑树的形式,纯链表的形式时间复杂度为O(n),红黑树则为O(logn),性能提升很大。什么时候链表转红黑树?当key值相等的元素形成的链表中元素个数超过8个的时候;当红黑树元素个数小于6个时会褪化为链表。

2.2 主要数据结构和关键变量

Node类:存放实际的key和value值

sizeCtl:

  • 负数:表示进行初始化或者扩容,-1表示正在初始化,-N,表示有N-1个线程正在进行扩容
  • 正数:0 表示还没有被初始化,>0的数,初始化或者是下一次进行扩容的阈值

TreeNode:用在红黑树,表示树的节点。

TreeBin:实际放在table数组中的,代表了这个红黑树的根

2.3 初始化

只是给成员变量赋值,put时进行实际数组的填充

2.4 定位元素

2.5 get方法

2.6 put方法

2.7 弱一致性

与JDK1.7一样还是存在弱一致性

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/418344.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

前端学习(1996)vue之电商管理系统电商系统之美化步骤条

目录结构 router.js import Vue from vue import Router from vue-router import Login from ./components/Login.vue import Home from ./components/Home.vue import Welcome from ./components/Welcome.vue import Users from ./components/user/Users.vue import Right fr…

前端学习(1997)vue之电商管理系统电商系统之渲染tab栏标签

目录结构 router.js import Vue from vue import Router from vue-router import Login from ./components/Login.vue import Home from ./components/Home.vue import Welcome from ./components/Welcome.vue import Users from ./components/user/Users.vue import Right fr…

LeetCode刷题过程中的一些小tips

0. 1. 发现没&#xff0c;与数组遍历、当前元素和前后元素大小比较 相关的都用单调栈 2. sql运行顺序查一下&#xff08;运行顺序依次是from、where、group by、select order by。先根据s1.Id进行分组&#xff0c;然后计算(count)组内大于等于score的个数(去重)&#xff0c;也…

Android之WebView网页滚动截图

WebView 网页滚动截屏&#xff0c;可对整个网页进行截屏而不是仅当前屏幕哦&#xff01; 注意若Web页面存在position:fixed; 的话得在调用前设置为 position:absolute; 哦&#xff0c;否则会出现很多次的&#xff0c;请看下面的具体解说吧&#xff01;&#xff01; private st…

前端学习(1998)vue之电商管理系统电商系统之实现步骤条和tab栏的数据

目录结构 router.js import Vue from vue import Router from vue-router import Login from ./components/Login.vue import Home from ./components/Home.vue import Welcome from ./components/Welcome.vue import Users from ./components/user/Users.vue import Right fr…

asp.net core 在Ubuntu 运行

asp.net core 在Ubuntu 运行 环境: Ubuntu 16.04dotnet-dev-1.0.0-preview2-003121Visual Studio 2015 update 3 Ubuntu 安装.net core 参考:https://www.microsoft.com/net/core#ubuntu 1.添加源 sudo sh -c echo "deb [archamd64] https://apt-mo.trafficmanager.net/re…

前端学习(1999)vue之电商管理系统电商系统之分析表单的数据

目录结构 router.js import Vue from vue import Router from vue-router import Login from ./components/Login.vue import Home from ./components/Home.vue import Welcome from ./components/Welcome.vue import Users from ./components/user/Users.vue import Right fr…

docker中的容器和镜像

最近学习了docker&#xff0c;感觉容器和镜像学的有点模糊。 特别是镜像和容器&#xff0c;感觉完全分不开&#xff0c;所以在此学习&#xff0c;然后总结了一下&#xff0c;便于后面的学习。 *************** 补充&#xff1a;经过我的一段时间使用&#xff0c;现在再来说一…

前端学习(2000)vue之电商管理系统电商系统之绘制基本面板的结构

目录结构 router.js import Vue from vue import Router from vue-router import Login from ./components/Login.vue import Home from ./components/Home.vue import Welcome from ./components/Welcome.vue import Users from ./components/user/Users.vue import Right fr…

微服务架构与组件总览

最近在各个地方总是看到微服务、消息队列、Redis、K8s等词语&#xff0c;下面就对他们涉及的概念进行一个总体的介绍&#xff0c;具体的技术实现目前还未完全掌握&#xff0c;那就先从整体把握关系&#xff0c;更方便以后的深入学习。(参考知乎和CSDN资料) 全篇以电商服务千万…

前端学习(2001)vue之电商管理系统电商系统之获取商品分类数据

目录结构 router.js import Vue from vue import Router from vue-router import Login from ./components/Login.vue import Home from ./components/Home.vue import Welcome from ./components/Welcome.vue import Users from ./components/user/Users.vue import Right fr…

shamir门限方案阅读与密码学课程感想

这里重点谈一下对Adi Shamir的关于阈值密钥分 享方案的论文《How to Share a Secret 》的理解&#xff0c;以及这两周密码学课堂的学习感想。 想要看懂一篇论文&#xff0c;首先要知道它是为了解决什么问题&#xff0c;然后看它为了解决这个问题采用了什么样 的方法&#xff0c…

前端学习(2002)vue之电商管理系统电商系统之绘制商品分类的级联选择器

目录结构 router.js import Vue from vue import Router from vue-router import Login from ./components/Login.vue import Home from ./components/Home.vue import Welcome from ./components/Welcome.vue import Users from ./components/user/Users.vue import Right fr…

jdbc建立数据库连接的helloword

package com.guoguo.db; import java.sql.DriverManager;import java.sql.ResultSet;import java.sql.SQLException; import com.mysql.jdbc.Connection;import com.mysql.jdbc.Statement; public class DBUtil { //数据库连接地址 private static final String URL "jdb…

前端学习(2003)vue之电商管理系统电商系统之之允许三级选择

目录结构 router.js import Vue from vue import Router from vue-router import Login from ./components/Login.vue import Home from ./components/Home.vue import Welcome from ./components/Welcome.vue import Users from ./components/user/Users.vue import Right fr…

搭建hexo博客并部署到github上

hexo是由Node.js驱动的一款快速、简单且功能强大的博客框架&#xff0c;支持多线程&#xff0c;数百篇文章只需几秒即可生成。支持markdown编写文章&#xff0c;可以方便的生成静态网页托管在github上。 感觉不错。 前端人员都在用github分享自己的代码。所以想着用hexo部署到g…

前端学习(2004)vue之电商管理系统电商系统之阻止页签切换

目录结构 router.js import Vue from vue import Router from vue-router import Login from ./components/Login.vue import Home from ./components/Home.vue import Welcome from ./components/Welcome.vue import Users from ./components/user/Users.vue import Right fr…

前端学习(2005)vue之电商管理系统电商系统之获取动态参数列表

目录结构 router.js import Vue from vue import Router from vue-router import Login from ./components/Login.vue import Home from ./components/Home.vue import Welcome from ./components/Welcome.vue import Users from ./components/user/Users.vue import Right fr…

Mysql存储结构B树与B+树与索引

首先要说明的是&#xff0c;B-树和B树是指同一个结构&#xff0c;并没有所谓的B减树&#xff0c;两种树是B-树和B树。 Mysql存储结构是一个B树。 1.存储结构与索引 众所周知&#xff0c;索引是关系型数据库中给数据库表中一列或多列的值排序后的存储结构&#xff0c;它是一种…

前端学习(2006)vue之电商管理系统电商系统之绘制商品参数的复选框

目录结构 router.js import Vue from vue import Router from vue-router import Login from ./components/Login.vue import Home from ./components/Home.vue import Welcome from ./components/Welcome.vue import Users from ./components/user/Users.vue import Right fr…