文章原文地址点击查看原文

首先HashMap是线程不安全的,其主要体现:1.在jdk1.7中,在多线程环境下,扩容时会造成环形链或数据丢失。2.在jdk1.8中,在多线程环境下,会发生数据覆盖的情况。

Java8可以把HashMap用在多线程中呢?

通过源码看到put/get方法都没有加同步锁,多线程情况最容易出现的就是:无法保证上一秒put的值,下一秒get的时候还是原值,所以线程安全还是无法保证。下图源码图:

有什么线程安全的类代替么?

在这样的场景,我们一般都会使用HashTable或者CurrentHashMapCollections.synchronizedMap(Map)创建线程安全的map集合。

但是因为前二者的并发度的原因基本上没啥使用场景了,所以存在线程不安全的场景我们都使用的是CorruentHashMap。他的性能和效率明显高于前两者。

(一)HashTable在方法上直接加锁,在多线程的过程中,一个线程在访问该方法,其中的线程都有等待,所以在多线程中不适用。HashTable源码图:

(二)Collections.synchronizedMap,在SynchronizedMap内部维护了一个普通对象Map,还有排斥锁mutex。Collections.synchronizedMap的源码图:

我们在调用这个方法的时候就需要传入一个Map,可以看到有两个构造器,如果你传入了mutex参数,则将对象排斥锁赋值为传入的对象。

如果没有,则将对象排斥锁赋值为this,即调用synchronizedMap的对象,就是上面的Map。

创建出synchronizedMap之后,再操作map的时候,就会对方法上锁,如图全是

Hashtable 跟HashMap的不同?

1、Hashtable 是不允许键或值为 null 的,HashMap 的键值则都可以为 null。

因为Hashtable在我们put 空值的时候会直接抛空指针异常,这是因为Hashtable使用的是安全失败机制(fail-safe),这种机制会使你此次读到的数据不一定是最新的数据。如果你使用null值,就会使得其无法判断对应的key是不存在还是为空,因为你无法再调用一次contain(key)来对key是否存在进行判断,ConcurrentHashMap同理。

但是HashMap却做了特殊处理。HashMap做处理的源码图:

2、实现方式不同:Hashtable 继承了 Dictionary类,而 HashMap 继承的是 AbstractMap 类。

3、初始化容量不同:HashMap 的初始容量为:16,Hashtable 初始容量为:11,两者的负载因子默认都是:0.75。

4、扩容机制不同当现有容量大于总容量 * 负载因子时,HashMap 扩容规则为当前容量翻倍,Hashtable 扩容规则为当前容量翻倍 + 1。

5、迭代器不同HashMap 中的 Iterator 迭代器是 fail-fast 的,而 Hashtable 的 Enumerator 不是 fail-fast 的。

快速失败(fail-fast)和安全失败(fail-safe)

一:快速失败(fail—fast)

在用迭代器遍历一个集合对象时,如果遍历过程中对集合对象的内容进行了修改(增加、删除、修改),则会抛出Concurrent Modification Exception。(并发修改异常)

原理:迭代器在遍历时直接访问集合中的内容,并且在遍历过程中使用一个 modCount 变量。集合在被遍历期间如果内容发生变化,就会改变modCount的值。每当迭代器使用hashNext()/next()遍历下一个元素之前,都会检测modCount变量是否为expectedmodCount值,是的话就返回遍历;否则抛出异常,终止遍历。

注意:这里异常的抛出条件是检测到 modCount!=expectedmodCount 这个条件。如果集合发生变化时修改modCount值刚好又设置为了expectedmodCount值,则异常不会抛出。因此,不能依赖于这个异常是否抛出而进行并发操作的编程,这个异常只建议用于检测并发修改的bug。

场景:java.util包下的集合类都是快速失败的,不能在多线程下发生并发修改(迭代过程中被修改)。

二:安全失败(fail—safe)

采用安全失败机制的集合容器,在遍历时不是直接在集合内容上访问的,而是先复制原有集合内容,在拷贝的集合上进行遍历。

原理:由于迭代时是对原集合的拷贝进行遍历,所以在遍历过程中对原集合所作的修改并不能被迭代器检测到,所以不会触发Concurrent Modification Exception。

缺点:基于拷贝内容的优点是避免了Concurrent Modification Exception,但同样地,迭代器并不能访问到修改后的内容,即:迭代器遍历的是开始遍历那一刻拿到的集合拷贝,在遍历期间原集合发生的修改迭代器是不知道的。

场景:java.util.concurrent包下的容器都是安全失败,可以在多线程下并发使用,并发修改。

快速失败和安全失败是对迭代器而言的。 快速失败:当在迭代一个集合的时候,如果有另外一个线程在修改这个集合,就会抛出ConcurrentModification异常,java.util下都是快速失败。 安全失败:在迭代时候会在集合二层做一个拷贝,所以在修改集合上层元素不会影响下层。在java.util.concurrent下都是安全失败

(三)ConcurrentHashMap数据结构

1.7中的数据结构是由 Segment 数组、HashEntry 组成,和 HashMap 一样,仍然是数组加链表

Segment 是 ConcurrentHashMap 的一个内部类,主要的组成如下:

static final class Segment<K,V> extends ReentrantLock implements Serializable {private static final long serialVersionUID = 2249069246763182397L;// 和 HashMap 中的 HashEntry 作用一样,真正存放数据的桶transient volatile HashEntry<K,V>[] table;transient int count;// 记得快速失败(fail—fast)么?transient int modCount;// 大小transient int threshold;// 负载因子final float loadFactor;}

HashEntry跟HashMap差不多的,但是不同点是,他使用volatile去修饰了他的数据Value还有下一个节点next。

volatile的特性是啥?1、保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。(实现可见性)2、禁止进行指令重排序。(实现有序性)3、volatile 只能保证对单次读/写的原子性。i++ 这种操作不能保证原子性。

ConcurrentHashMap为什么并发度高?

原理上来说,ConcurrentHashMap 采用了分段锁技术,其中 Segment 继承于 ReentrantLock。不会像 HashTable 那样不管是 put 还是 get 操作都需要做同步处理,理论上 ConcurrentHashMap 支持 CurrencyLevel (Segment 数组数量)的线程并发。每当一个线程占用锁访问一个 Segment 时,不会影响到其他的 Segment。就是说如果容量大小是16他的并发度就是16,可以同时允许16个线程操作16个Segment而且还是线程安全的。

public V put(K key, V value) {Segment<K,V> s;if (value == null)throw new NullPointerException();//这就是为啥他不可以put null值的原因int hash = hash(key);int j = (hash >>> segmentShift) & segmentMask;if ((s = (Segment<K,V>)UNSAFE.getObject          (segments, (j << SSHIFT) + SBASE)) == null) s = ensureSegment(j);return s.put(key, hash, value, false);
}

他先定位到Segment,然后再进行put操作。我们看看他的put源代码,你就知道他是怎么做到线程安全的了。

final V put(K key, int hash, V value, boolean onlyIfAbsent) {// 将当前 Segment 中的 table 通过 key 的 hashcode 定位到 HashEntryHashEntry<K,V> node = tryLock() ? null :scanAndLockForPut(key, hash, value);V oldValue;try {HashEntry<K,V>[] tab = table;int index = (tab.length - 1) & hash;HashEntry<K,V> first = entryAt(tab, index);for (HashEntry<K,V> e = first;;) {if (e != null) {K k;// 遍历该 HashEntry,如果不为空则判断传入的 key 和当前遍历的 key 是否相等,相等则覆盖旧的 value。if ((k = e.key) == key ||(e.hash == hash && key.equals(k))) {oldValue = e.value;if (!onlyIfAbsent) {e.value = value;++modCount;}break;}e = e.next;}else {// 不为空则需要新建一个 HashEntry 并加入到 Segment 中,同时会先判断是否需要扩容。if (node != null)node.setNext(first);elsenode = new HashEntry<K,V>(hash, key, value, first);int c = count + 1;if (c > threshold && tab.length < MAXIMUM_CAPACITY)rehash(node);elsesetEntryAt(tab, index, node);++modCount;count = c;oldValue = null;break;}}} finally {//释放锁unlock();}return oldValue;
}

首先第一步的时候会尝试获取锁,如果获取失败肯定就有其他线程存在竞争,则利用 scanAndLockForPut() 自旋获取锁。

  1. 尝试自旋获取锁。

  2. 如果重试的次数达到了 MAX_SCAN_RETRIES 则改为阻塞锁获取,保证能获取成功。

那他是如何get的逻辑呢??

将 Key 通过 Hash 之后定位到具体的 Segment ,再通过一次 Hash 定位到具体的元素上。

由于 HashEntry 中的 value 属性是用 volatile 关键词修饰的,保证了内存可见性,所以每次获取时都是最新值。

ConcurrentHashMap 的 get 方法是非常高效的,因为整个过程都不需要加锁

jdk1.7虽然可以支持每个Segment并发访问,但是还存在一些问题?

是的,因为基本上还是数组加链表的方式,我们去查询的时候,还得遍历链表,会导致效率很低,这个跟jdk1.7的HashMap是存在的一样问题,所以他在jdk1.8完全优化了。

jdk1.8它的数据结构是怎么样子的呢?

其中抛弃了原有的 Segment 分段锁,而采用了 CAS + synchronized 来保证并发安全性。跟HashMap很像,也把之前的HashEntry改成了Node,但是作用不变,把值和next采用了volatile去修饰,保证了可见性,并且也引入了红黑树,在链表大于一定值的时候会转换(默认是8)。

jdk1.8之后它值的存取操作么?以及是怎么保证线程安全的?

ConcurrentHashMap在进行put操作的还是比较复杂的,大致可以分为以下步骤:

  1. 根据 key 计算出 hashcode 。

  2. 判断是否需要进行初始化。

  3. 即为当前 key 定位出的 Node,如果为空表示当前位置可以写入数据,利用 CAS 尝试写入,失败则自旋保证成功。

  4. 如果当前位置的 hashcode == MOVED == -1,则需要进行扩容。

  5. 如果都不满足,则利用 synchronized 锁写入数据。

  6. 如果数量大于 TREEIFY_THRESHOLD 则要转换为红黑树。

下图为源码图:

CAS是什么?自旋又是什么?

CAS 是乐观锁的一种实现方式,是一种轻量级锁,JUC 中很多工具类的实现就是基于 CAS 的。线程在读取数据时不进行加锁,在准备写回数据时,比较原值是否修改,若未被其他线程修改则写回,若已被修改,则重新执行读取流程。

这是一种乐观策略,认为并发操作并不总会发生。CAS 操作的流程如下图所示,

CAS就一定能保证数据没被别的线程修改过么?

并不是的,比如很经典的ABA问题,CAS就无法判断了。

什么是ABA?

就是说来了一个线程把值改回了B,又来了一个线程把值又改回了A,对于这个时候判断的线程,就发现他的值还是A,所以他就不知道这个值到底有没有被人改过,其实很多场景如果只追求最后结果正确,这是没关系的。但是实际过程中还是需要记录修改过程的,比如资金修改什么的,你每次修改的都应该有记录,方便回溯。

如何解决ABA问题?

用版本号去保证就好了,就比如说,我在修改前去查询他原来的值的时候再带一个版本号,每次判断就连值和版本号一起判断,判断成功就给版本号加1。

update a set value = newValue ,vision = vision + 1 where value = #{oldValue} and vision = #{vision} // 判断原来的值和版本号是否匹配,中间有别的线程修改,值可能相等,但是版本号100%不一样

其实有很多方式,比如时间戳也可以,查询的时候把时间戳一起查出来,对的上才修改并且更新值的时候一起修改更新时间,这样也能保证,方法很多但是跟版本号都是异曲同工之妙,看场景大家想怎么设计吧。

CAS性能很高,但是我知道synchronized性能可不咋地,为啥jdk1.8升级之后反而多了synchronized?

synchronized之前一直都是重量级的锁,但是后来java官方是对他进行过升级的,他现在采用的是锁升级的方式去做的。针对 synchronized 获取锁的方式,JVM 使用了锁升级的优化方式,就是先使用偏向锁优先同一线程然后再次获取锁,如果失败,就升级为 CAS 轻量级锁,如果失败就会短暂自旋,防止线程被系统挂起。最后如果以上都失败就升级为重量级锁。所以是一步步升级上去的,最初也是通过很多轻量级的方式锁定的。

ConcurrentHashMap的get操作又是怎么样子的呢?

  • 根据计算出来的 hashcode 寻址,如果就在桶上那么直接返回值。

  • 如果是红黑树那就按照树的方式获取值。

  • 就不满足那就按照链表的方式遍历获取值。

HashMap为什么线程不安全?(附源码)相关推荐

  1. 使用redis的setnx可以非同一线程进行加锁和解锁(附源码)

    使用redis的setnx可以非同一线程进行加锁和解锁(附源码) 问题背景 项目搭建 总结 Lyric: 那在终点之前 问题背景 Redisson做分布式锁是目前比较流行的方式,但是在使用的过程中遇到 ...

  2. 炫酷,SpringBoot+Echarts实现用户访问地图可视化(附源码)

    点击上方"方志朋",选择"设为星标" 回复"666"获取新整理的面试文章 SpringBoot+Echarts用户访问地图可视化 意义 在常 ...

  3. 推荐一套开源通用后台管理系统(附源码)

    今日推荐 Java 8 一行代码解决了空指针问题,太厉害了...List中remove()方法的陷阱,被坑惨了!25000 字详解 23 种设计模式,原来可以这么简单!最牛逼的 Java 日志框架,性 ...

  4. DSP:6678开发板NDK网口通信完整实现(附源码)

    如果出现图片打不开,或是显示异常,请点击下方链接阅读原文!!! DSP:6678开发板NDK网口通信完整实现(附源码) - 子木的文章 - 知乎 https://zhuanlan.zhihu.com/ ...

  5. apache poi 修改docx表格_一个excel(20M)就能干趴你的poi,你信吗(附源码)?

    点击上方"阿拉奇学Java",选择"置顶或者星标" 优质文章第一时间送达! 链接: www.cnblogs.com/rongdi/p/11872810.html ...

  6. Spring Web Flow 入门demo(三)嵌套流程与业务结合 附源码

    转载地址 ; http://blog.csdn.net/hejingyuan6/article/details/46723021 上篇博客我们说spring web Flow与业务结合的方式主要有三种 ...

  7. 天天酷跑php源码_使用Java实现天天酷跑(附源码)

    首先,写一个需求文档: 一.项目名称:<天天酷跑>(RunDay) 二.功能介绍: 闯关类游戏,玩家登录后,选择进入游戏,通过键盘控制玩家的上下左右移动,来躲避 障碍物和吃金币,玩家躲避的 ...

  8. EJB+JSF开发示例(附源码)

    刚到公司时没有接触过EJB3和JSF,为了熟悉一下做了个EJB+JSF开发示例来入门.一个比较简单的JSF的web工程做页面展现,一个简单的EJB3工程做业务操作,分别部署到Tomcat下和JBoss ...

  9. CTP接口开发案例(内附源码)

    CTP接口开发(内附源码) 提示:在看本博客之前建议先阅读上期所官方的开发文档(SimNow官网中去下载CTP接口文件),然后在SimNow官网注册模拟账号. 提示:股票CTP接口和期货CTP接口类似 ...

  10. Android根据音量大小绘制心电图(附源码)

    前提:很久没有更新了,今天就为大家带来一个画布和SurfaceView的使用吧.(附源码资源) 需求:录制音频时有时候为了增加与用户的体验,我们需要增加与用户的交互,于是就有了录音动画.如下图 ) 分 ...

最新文章

  1. 软件测试基础--笔记6
  2. javascript中作用域,优先级等等问题, 求助中。。。。。。。。
  3. 转置型FIR滤波器的fpga实现
  4. Internet Explorer 已停止工作 解决办法
  5. 基于注解的SpringMVC整合JPA
  6. linux服务器选ubantu或centos_如何通过SSH连接阿里云上的Linux系统
  7. 花5分钟时间来了解一下高性能网关Kong会有意外收获
  8. YII 规则rule 里面 min,max 提示错误信息
  9. 道路建立拓扑关系的规则_建立真正社区关系的5条规则
  10. 音乐人高嘉丰在 Opensea 发布音乐 NFT
  11. Mysql中key与index区别
  12. DWM1000的UWB测距改官网例程的调试
  13. KafKa - 集群模式搭建
  14. DW标签使用与javascript文档基础介绍
  15. SII9136添加HDCP功能
  16. 【云原生之K8s】 Pod基础概念
  17. python人脸特征提取_Python实现识别人脸特征并打印出来
  18. (三)feild ii 相控阵聚焦成像:RF信号到成像全过程
  19. 网状结构的“数据”区域
  20. 国内IT运维管理软件五大发展趋势

热门文章

  1. 【递归】两道程序题理解递归
  2. 《格蠹汇编》读书笔记—windbg的使用(中)
  3. 探索用户行为(淘宝用户行为分析)
  4. 拼了(求婚事务所电视原声带)铃声 拼了(求婚事务所电视原声带)...
  5. 外卖平台的设计与实现
  6. Android高级架构师整理面试经历发现?(大厂面经,移动端静态网站开发
  7. [巩固C#] 一、特性是什么东东
  8. 基于CocosCreator的切水果小游戏(一)
  9. 解决 应用程序无法正常启动0xc0150002
  10. QPushbutton之setProperty