目录

一、多线程使用HashMap的一些线程安全问题

①造成数据新增丢失

②扩容时候,造成链表成环(jdk1.7版本)

二、Hashtable和HashMap的区别

①核心方法加锁

②其他语法上面的略微差异

三、引入ConcurrentHashMap【重要】

①ConcurrentHashMap相比于Hashtable的优势

Hashtable的缺点:

​编辑

ConcurrentHashMap的一些优化措施(jdk1.8往后)

(1)每一个哈希桶,都是一把"锁"

​编辑

(2)ConcurrentHashMap不针对get(Object key)方法加锁

(3)ConcurrentHashMap针对部分修改操作,采用了volatile+原子的方式,让写的操作变为"原子"的,从而与不加锁的读操作不会产生锁冲突

(4)扩容操作的优化

②关于ConcurrentHashMap线程安全问题的一些思考

get与put方法存在的一些"问题":

size与put方法存在的一些问题:

因此,对于ConcurrentHashMap来说,实现元素+1的思路大致是:

四、ConcurrentHashMap的jdk1.7和1.8版本的区别

区别1:(最重要的区别):锁的粒度

区别2:put的执行流程有所不同

区别3:计算size的方式不一样


这三种数据结构,都是对于哈希表的具体实现。

有关哈希表的具体设计,我们在前面的文章当中也提到了,关于HashMap的简单源码分析,以及HashMap的具体的一些实现,也在下面这两篇文章当中提到了:

(1条消息) HashMap简单源码分析_革凡成圣211的博客-CSDN博客https://blog.csdn.net/weixin_56738054/article/details/127786631?spm=1001.2014.3001.5501
(1条消息) Java的手写简单的哈希表_革凡成圣211的博客-CSDN博客https://blog.csdn.net/weixin_56738054/article/details/127416821?spm=1001.2014.3001.5501


下面,将重点分析,多线程使用HashMap的一些问题,以及Hashtable、ConcurrentHashMap和HashMap之间的一些区别。


一、多线程使用HashMap的一些线程安全问题

①造成数据新增丢失

因为HashMap当中,并没有涉及任何的加锁操作,因此:当多个线程同时调用put()的时候,有可能在两个键的哈希值一样的时候,之后调用put()的线程新增的值覆盖掉最开始线程新增的值。

图解:


②扩容时候,造成链表成环(jdk1.7版本)

由于jdk1.7版本以及其之前,是采用头插法的方式进行插入的,因此,会造成链表成环的问题;

扩容的操作,实际上是HashMap重现初始化一个原来大小2倍的数组,并且根据新的数组的长度,重新哈希的这样一个过程。

如果执行并发扩容,那么,很有可能在扩容的时候,让哈希表当中某一个哈希桶的链表变成了一个"环"。那么,也就意味着如果想获取某一个元素,对哈希桶对应位置的链表进行遍历的时候,没有任何一个节点的next指针为null,那么将引发死循环。

对于jdk1.8版本以及之后,采用的就是尾插法,因此不会造成链表成环。但是仍然有新增数据丢失的问题。


二、Hashtable和HashMap的区别

①核心方法加锁

Hashtable的核心方法put()和get()方法被加锁了。因此,Hashtable是线程安全的


②其他语法上面的略微差异

(1)HashMap允许null值作为键和值,而Hashtable不允许null作为键和值。

(2)添加值的哈希算法不同,对于HashMap来说,添加值的哈希算法采用的是内部自定义的一个哈希算法,而Hashtable采用的直接是key.hash()的方式计算出的哈希值。但是负载因子都是0.75.

(3)初始化的容量不同:HashMap的默认初始容量为16,而Hashtable默认的初始容量为11.

(4)扩容的方式不同:HashMap采用的是2倍大小的方式扩容,而Hashtable扩容的规则为当前容量*2+1.

...这些差异,罗列一部分即可,最重要的还是多线程和单线程的使用环境区别。


三、引入ConcurrentHashMap【重要】

①ConcurrentHashMap相比于Hashtable的优势

对于Hashtable来说,它解决线程安全问题的方式,比较"粗鲁”。

Hashtable的缺点:


第一:

Hashtable使用的直接是synchronized修饰核心方法的方式来加锁的,那么,如果两个线程同时只是读取某一个变量的值,根据之前对线程安全问题的概述,如果线程仅仅只是对变量进行操作而并非操作,那么并不会发生线程安全问题。

但是Hashtable即使是在多个线程同时读取某个Entry的值的时候,也照样会造成阻塞等待的情况,因此Hashtable的锁的粒度是比较大的。


第二:

       即使是put()、get()操作,发生线程安全问题的前提条件也必须是需要put()的两个键的哈希值相同的情况,也就是,针对同一个哈希桶进行put()或者get()的情况。

而Hashtable采用的是直接“一棒打死”。无论是否针对同一个哈希桶进行读写操作,只要多个线程同时调用一个map的put()或者get()方法,都会发生阻塞等待的情况。


ConcurrentHashMap的一些优化措施(jdk1.8往后)

(1)每一个哈希桶,都是一把"锁"

让每一个哈希桶都是一把锁。当新增元素发生哈希冲突,也就是散列到同一个哈希地址的时候,才会发生锁冲突。

这样,就有效减少了不必要的锁冲突,减小了锁的粒度

观察上面的图:当两个线程同时尝试分别修改同一个哈希地址的1,2节点的时候,会产生阻塞等待的情况。

当两个线程同时修改3,4节点的时候,不会产生阻塞等待。

也就是,只有发生了哈希冲突的时候,才会产生阻塞等待的情况 

观察一下源码:


对于jdk1.8之前的代码,是采用分段锁的方式进行修改的。也就是,其中的N个哈希桶作为一把锁,如果有线程同时针对这N个为一把锁的哈希桶进行修改操作,会产生锁冲突,造成阻塞等待。


(2)ConcurrentHashMap不针对get(Object key)方法加锁

由于get()方法是通过key得到对应的value的值的方法,本质上是“读取”操作,当多个线程同时调用get()方法的时候,不存在线程安全问题。

因此,ConcurrentHashMap取消了对于get()方法加锁的机制。

这里需要注意的地方是,ConcurrentHashmap当中:

Ⅰ一个线程去数据,另外一个线程也同时去取数据,这个时候不存在线程安全问题,也不存在锁冲突;因为ConcurrentHashMap没有针对get方法加锁

Ⅱ一个线程去数据,另外一个线程也去数据,不存在线程安全问题,但是有可能存在锁冲突;存在锁冲突的前提是两个线程针对同一个哈希桶进行写操作。

        Ⅲ一个线程去数据,另外一个线程去数据,不存在线程安全问题,但是有可能产生锁冲突。

        对于第Ⅲ点,如果产生了锁冲突,那么就是意味着两个线程一个对于同一哈希桶进行"读"操作,另外一个针对哈希桶进行"写"操作,并且都是哈希桶有存放元素,也就是需要遍历的时候,才会产生锁冲突。

如果哈希桶没有存放元素,是null的,那么,请往下面第(3)点查看。


(3)ConcurrentHashMap针对部分修改操作,采用了volatile+原子的方式,让写的操作变为"原子"的,从而与不加锁的读操作不会产生锁冲突

ConcurrentHashMap内部充分利用了CAS,来削减了加锁的次数。

下面,举几个例子,关于ConcurrentHashMap是如何使用CAS的:

例子1、当让存放元素之后,个数+1的时候,采用的是CAS的做法来保证数组当中实际存储key的数量+1这个操作的原子性

其中,addCount方法内部采用的就是CAS的做法来实现个数+1的原子性的。


例子2、 当对应的HashEntry数组当中,某一个位置不存放任何元素,也就是为null的时候,会采用cas的方式填充到对应的哈希地址。

       对于ConcurrentHashMap当中修改key的个数,本质上还是在addCount方法内部,通过CAS的方式来修改


(4)扩容操作的优化

回顾一下HashMap或者Hashtable扩容的操作,它们都是创建一个更大容量的数组,然后把每一个元素重新哈希的做法。

在数据量比较大的时候,会造成可能某次put()之后,线程会阻塞等待很长的时间,才可以完成扩容。

扩容条件:

①当存放Node<K,V>节点的数组长度小于64,并且单个哈希桶的链表的存放节点个数达到8的时候,会触发扩容;如果存放节点的数组长度>=64,那么会把当前的链表树化为红黑树。

②当存储的实际key的数量/Node<K,V>数组的长度达到负载因子的时候,会触发扩容


以上两点,和jdk1.8版本的HashMap的扩容前提条件类似,没有太大的差别。

满足上面的条件之后,会进入到下面的操作:

首先申请一个原来数组大小2倍的新数组

如果有多个线程同时尝试扩容,那么,ConcurrentHashMap会对这些线程进行"分工”。何为分工呢?画个图简单看一看:

也就是,每一个线程,分别对原有的数组上面的元素分别进行"搬运"操作,让它们都被各自的线程重新哈希到新的数组上面的位置,这样的效率会更加的高。


同时,如果ConcurrentHashMap如果正在扩容的时候:

其他线程调用get方法,那么调用的线程会查询旧数组新的数组当中是否存在对应的key。

其他线程如果调用remove方法,那么调用的线程会把旧数组和新的数组当中的key都删除掉。

而不是一直阻塞等待,直到扩容执行完毕。


②关于ConcurrentHashMap线程安全问题的一些思考

get与put方法存在的一些"问题":

问题:现在有这样的一个场景:一个ConcurrentHashMap当中,某一处的哈希桶上面有不止一个的元素:如图:

假如,此时,有一个线程执行一条对于Node2节点的修改操作指令:

map.put(k2,v3);

另外一个线程同时执行读取操作的指令:

 map.get(k2);

结合前面介绍的ConcurrentHashMap的知识,可以了解到,ConcurrentHashMap不针对get()方法加锁:结合源码,也可以看出来

get方法源码:

再看一下,对于put方法当中,对于已经存在的key的修改value操作

put方法源码

可以看到,ConcurrentHashMap当中,虽然更新value的操作位于同步代码块当中,但是,由于读取操作get方法没有加锁,并且,此处采用的更新value的操作是直接e.value=value。那么,这样,就有可能导致,调用get方法的线程读取到的是一个还没有来得及更新的值——v2。


   图解(模拟出现"线程安全"问题的线程调度情况):

时间轴 线程1 线程2
t1 携带(k2,v3)进入put方法内部
t2

执行到更新value的代码之前

t3 进入get方法内部,调用get(k2)
t4 通过k2得到v2的值,然后返回
t5 执行把v2更新为v3的代码

可以看到,在t4时刻,线程2得到了一个"脏数据"——v2。


对于上述操作,看似存在线程安全问题,实际上,这种做法也是可以采取的。

原因呢:本人认为有以下两点:

①ConcurrentHashMap的设计初衷就是追求极致的效率,因此它就是需要减少加锁的粒度。

②即使真的发生了上述的情况,那么get的时候,要么读取到更新之前的值v2,要么读取到更新之后的值v3不会get到其他的value。因此,在一定的场景下面,也可以认为是线程安全的。


size与put方法存在的一些问题:

同理,ConcurrentHashMap当中的size()方法也存在类似的问题:

在put方法的尾部,可以看到ConcurrentHashMap是使用addCount方法来实现元素个数+1

这个addCount内部虽然采用的是CAS的方式来实现+1的,但是,如果执行put方法的线程还没有执行addCount方法,就被调度离开CPU内核,此时另外一个线程调用size方法来获取key的个数,也会读取到"脏数据",也就是更新之前的数据。

 图解(假如此时ConcurrentHashMap当中只有3个key)

时间轴 线程1 线程2
t1 进入put方法内部
t2 执行完插入操作,但是在执行addCount之前被调度离开cpu内核
t3 调用size方法获取key的个数:3
t4 重新回到cpu内核,执行addCount方法,让key变为4
t5 返回

可以看到,此时线程2在t3时刻得到的key的个数为3,是一个脏数据。

但是,上述情况也被认为是线程安全的,因为,addCount方法内部采用的是CAS的做法。因此,不会造成多个线程同时调用+1而造成增加数量丢失过大的情况。

即使其他线程调用size方法,要么获取到的是更细你个数之前的值,要么是执行一次则增之后的


下面,我们来看一下size()方法的源码

因此,对于ConcurrentHashMap来说,实现元素+1的思路大致是:

当线程竞争不激烈的时候:直接在addCount方法内部使用CAS的方式来实现元素个数(baseCount)的值+1.

当线程竞争激烈的时候:使用CounterCell数组,然后采用分而治之的方式来统计总的元素个数。然后遍历CountCell数组里面的value的值,进行累加

可以看到,size方法并没有实现和put方法的同步锁,因此它是线程不安全的。


为什么要这样设计,不像Hashtable这样加同步锁?

原因1:CoucurrentHashMap追求性能的高效,如果直接在size方法上面加锁有可能影响性能

原因2: 在counurrentHashMap的设计理念当中,对于size的数据要求严格性不高。主要还是对于数据存储的安全性要求比较高。


四、ConcurrentHashMap的jdk1.7和1.8版本的区别

区别1:(最重要的区别):锁的粒度

jdk1.7采用的是采用Segment+HashEntry数组的方式来处理线程安全问题的。也就是采用分段锁的方式来加锁的。

而jdk1.8取消了分段锁的机制,而是把每一个哈希桶当中的头节点都作为一把锁。并且采用的方式为synchronized+CAS+红黑树的方式

这样的措施,减小了锁的粒度,增加了并发度(同一时间支持访问一个map的线程数量)


区别2:put的执行流程有所不同

jdk1.7当中,ConcurrentHashMap要进行两次定位:

先对Segment进行定位,再对于数组下标进行定位。定位成功之后,采用自旋锁的方式获取锁。

如果自旋次数超过64,就会发生膨胀,令put的线程进入阻塞状态,等待唤醒。

而jdk1.8,只需要1次定位:

如果对应的Node数组内部的哈希地址为null,那么采用CAS的方式插Node节点。

如果不为null,说明该位置有元素了,需要加锁,锁住头节点。


区别3:计算size的方式不一样

对于jdk1.7:采用乐观锁的机制。

首先进行遍历,不是直接加锁。如果两次遍历的结果一致,那么直接返回。

如果不一致,那么对每个Segment加锁,再遍历一次,然后返回。

对于jdk1.8的size()

维护一个baseCount属性来记录节点的数量。每put一次就CAS自增一次。通过CAS来确保线程安全的自增。

HashMap,Hashtable,ConcurrentHashMap相关推荐

  1. HashMap,HashTable,ConcurrentHashMap面试总结!!!

    原文:https://www.cnblogs.com/hexinwei1/p/10000779.html 一.小总结 1.HashMap .HashTable. ConcurrentHashMap H ...

  2. HashMap HashTable ConcurrentHashMap

    1. Hashtable 和 HashMap (1)区别,这两个类主要有以下几方面的不同: Hashtable和HashMap都实现了Map接口,但是Hashtable的实现是基于Dictionary ...

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

    http://www.cnblogs.com/aspirant/p/6856487.html 转载于:https://www.cnblogs.com/wuMing-dj/p/6868817.html

  4. hashmap hashtable concurrenthashmap区别

    https://www.cnblogs.com/heyonggang/p/9112731.html

  5. Hashtable,HashMap,ConcurrentHashMap都是Map的实现类,它们在处理null值的存储上有细微的区别,下列哪些说法是正确的

    多选 Hashtable,HashMap,ConcurrentHashMap都是Map的实现类,它们在处理null值的存储上有细微的区别,下列哪些说法是正确的:答案在文末 A. Hashtable的K ...

  6. HashMap HashTable和ConcurrentHashMap的区别

    HashMap和Hashtable的区别 HashMap和Hashtable都实现了Map接口,但决定用哪一个之前先要弄清楚它们之间的分别.主要的区别有:线程安全性,同步(synchronizatio ...

  7. 集合之比较接口器+Map家族的HashMap+LinkedHashMap+Hashtable+ConcurrentHashMap

    集合之比较接口器+Map家族的HashMap+LinkedHashMap+Hashtable+ConcurrentHashMap 一.比较器接口 1.内置比较器 – Comparable import ...

  8. hashmap的特性?HashMap底层源码,数据结构?Hashmap和hashtable ConcurrentHashMap区别?

    1.hashmap的特性? 允许空键和空值(但空键只有一个,且放在第一位) 元素是无序的,而且顺序会不定时改变 key 用 Set 存放,所以想做到 key 不允许重复,key 对应的类需要重写 ha ...

  9. 分析HashTable、HashMap、ConcurrentHashMap的结构、初始化及扩容机制

    目录 一.前沿 二.HashTable 三.HashMap Jdk1.7 Jdk1.8 四.ConcurrentHashMap Jdk1.7 Jdk1.8 五.总结 一.前沿 相信很多同学对HashT ...

最新文章

  1. Android 双击返回键退出程序 实现
  2. gzcms技术开发文档
  3. 华为内部面试题库---(10)
  4. JAVA——DES/ECB/PKCS7Padding加密算法[Cannot find any provider supporting DESEDE/CBC/PKCS7Padding]解决方案
  5. vue获取前一个页面路由地址
  6. 为什么BDLocationListener没有被调用
  7. Qt Console Application 与 Qt GUI Application互转
  8. 手写 Promise.all
  9. Windows 文件一直被占用,无法删除(对应解决方法)
  10. xilinx sdk打开库函数查看具体定义Open Declaration或者ctrl+鼠标左键
  11. Visual studio 2013 添加 GitHub
  12. java实现语法分析实验报告_[源码和文档分享]基于JAVA实现的基于LL(1)语法分析构造表...
  13. linux node安装菜鸟教程,Docker 安装 Node.js
  14. 微信小程序中长按识别二维码
  15. asp.net 视频教程
  16. 腾讯笔试经验-不是大牛-勿看
  17. 适用于 Windows 7 SP1 和 Windows Server 2008 R2 SP1 的扩展安全更新(ESU)许可准备程序包
  18. Matlab进行t检验
  19. 恢复matlab文件关联
  20. HashMap中的hash与rehash

热门文章

  1. axure9中继器使用
  2. 矢量图标文件格式设计
  3. DeBank 的钱包 Rabby Swap 智能合约被利用
  4. 荔枝IPO背后,还有4个大坑待填
  5. Dos命令后台运行Jar包
  6. Spring Cloud Alibaba(13)---Sleuth概述
  7. 史上最全的画册制作流程—设计
  8. 目标检测究竟发展到了什么程度?| 目标检测发展22年
  9. STC89C52 51单片机实现闹钟功能 #目前的时间倒计时 #选择位置闪烁 #计时 #清屏 #停止计时 #选择位置加减数字 #存储目前的数据 #读出目前的数据 #输入特定时间
  10. Windows11系统导航栏不见了.