扫二维码与项目经理沟通
我们在微信上24小时期待你的声音
解答本文疑问/技术咨询/运营咨询/技术建议/互联网交流
目录
创新互联成立于2013年,我们提供高端成都网站建设、网站制作、网站设计、网站定制、成都营销网站建设、微信小程序开发、微信公众号开发、成都网站推广服务,提供专业营销思路、内容策划、视觉设计、程序开发来完成项目落地,为成都发电机租赁企业提供源源不断的流量和订单咨询。一、多线程使用HashMap的一些线程安全问题
①造成数据新增丢失
②扩容时候,造成链表成环
二、Hashtable和HashMap的区别
①核心方法加锁
②其他语法上面的略微差异
三、引入ConcurrentHashMap【重要】
①ConcurrentHashMap相比于Hashtable的优势
Hashtable的缺点:
ConcurrentHashMap的一些优化措施(jdk1.8往后)
(1)每一个哈希桶,都是一把"锁"
编辑
(2)ConcurrentHashMap不针对get(Object key)方法加锁
(3)ConcurrentHashMap针对部分修改操作,采用了volatile+原子的方式,让写的操作与不加锁的get()方式不会产生锁冲突
(4)扩容操作的优化
这三种数据结构,都是对于哈希表的具体实现。
有关哈希表的具体设计,我们在前面的文章当中也提到了,关于HashMap的简单源码分析,以及HashMap的具体的一些实现,也在下面这两篇文章当中提到了:
(1条消息) HashMap简单源码分析_革凡成圣211的博客-博客https://blog.csdn.net/weixin_56738054/article/details/127786631?spm=1001.2014.3001.5501
(1条消息) Java的手写简单的哈希表_革凡成圣211的博客-博客https://blog.csdn.net/weixin_56738054/article/details/127416821?spm=1001.2014.3001.5501
下面,将重点分析,多线程使用HashMap的一些问题,以及Hashtable、ConcurrentHashMap和HashMap之间的一些区别。
因为HashMap当中,并没有涉及任何的加锁操作,因此:当多个线程同时调用put()的时候,有可能在两个键的哈希值一样的时候,之后调用put()的线程新增的值覆盖掉最开始线程新增的值。
图解:
我们了解到,扩容的操作,实际上是HashMap重现初始化一个原来大小2倍的数组,并且根据新的数组的长度,重新哈希的这样一个过程。
如果执行并发扩容,那么,很有可能在扩容的时候,让哈希表当中某一个哈希桶的链表变成了一个"环"。那么,也就意味着如果想获取某一个元素,对哈希桶对应位置的链表进行遍历的时候,没有任何一个节点的next指针为null,那么将引发死循环。
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.
...这些差异,罗列一部分即可,最重要的还是多线程和单线程的使用环境区别。
对于Hashtable来说,它解决线程安全问题的方式,比较"粗鲁”。
Hashtable的缺点:第一:
Hashtable使用的直接是synchronized修饰核心方法的方式来加锁的,那么,如果两个线程同时只是读取某一个变量的值,根据之前对线程安全问题的概述,如果线程仅仅只是对变量进行读操作而并非写操作,那么并不会发生线程安全问题。
但是Hashtable即使是在多个线程同时读取某个Entry的值的时候,也照样会造成阻塞等待的情况,因此Hashtable的锁的粒度是比较大的。
第二:
即使是put()、get()操作,发生线程安全问题的前提条件也必须是需要put()的两个键的哈希值相同的情况,也就是,针对同一个哈希桶进行put()或者get()的情况。
而Hashtable采用的是直接“一棒打死”。无论是否针对同一个哈希桶进行读写操作,只要多个线程同时调用一个map的put()或者get()方法,都会发生阻塞等待的情况。
让每一个哈希桶都是一把锁。当新增元素发生哈希冲突,也就是散列到同一个哈希地址的时候,才会发生锁冲突。
这样,就有效减少了不必要的锁冲突,减小了锁的粒度
观察上面的图:当两个线程同时尝试分别修改同一个哈希地址的1,2节点的时候,会产生阻塞等待的情况。
当两个线程同时修改3,4节点的时候,不会产生阻塞等待。
也就是,只有发生了哈希冲突的时候,才会产生阻塞等待的情况
观察一下源码:
对于jdk1.8之前的代码,是采用分段锁的方式进行修改的。也就是,其中的N个哈希桶作为一把锁,如果有线程同时针对这N个为一把锁的哈希桶进行修改操作,会产生锁冲突,造成阻塞等待。
由于get()方法是通过key得到对应的value的值的方法,本质上是“读取”操作,当多个线程同时调用get()方法的时候,不存在线程安全问题。
因此,ConcurrentHashMap取消了对于get()方法加锁的机制。
这里需要注意的地方是,ConcurrentHashmap当中:
Ⅰ一个线程读去数据,另外一个线程也同时去读取数据,这个时候不存在线程安全问题,也不存在锁冲突;因为ConcurrentHashMap没有针对get方法加锁
Ⅱ一个线程去写数据,另外一个线程也去写数据,不存在线程安全问题,但是有可能存在锁冲突;存在锁冲突的前提是两个线程针对同一个哈希桶进行写操作。
Ⅲ一个线程去读数据,另外一个线程去写数据,不存在线程安全问题,但是有可能产生锁冲突。
对于第Ⅲ点,如果产生了锁冲突,那么就是意味着两个线程一个对于同一哈希桶进行"读"操作,另外一个针对哈希桶进行"写"操作,并且都是哈希桶有存放元素,也就是需要遍历的时候,才会产生锁冲突。
如果哈希桶没有存放元素,是null的,那么,请往下面第(3)点查看。
ConcurrentHashMap内部充分利用了CAS,来削减了加锁的次数。
下面,举几个例子,关于ConcurrentHashMap是如何使用CAS的:
例子1、当让存放元素之后,个数+1的时候,采用的是CAS的做法来保证数组当中实际存储key的数量+1这个操作的原子性
其中,addCount方法内部采用的就是CAS的做法来实现个数+1的原子性的。
例子2、 当对应的HashEntry数组当中,某一个位置不存放任何元素,也就是为null的时候,会采用cas的方式填充到对应的哈希地址。
对于ConcurrentHashMap当中修改key的个数,本质上还是在addCount方法内部,通过CAS的方式来修改
回顾一下HashMap或者Hashtable扩容的操作,它们都是创建一个更大容量的数组,然后把每一个元素重新哈希的做法。
在数据量比较大的时候,会造成可能某次put()之后,线程会阻塞等待很长的时间,才可以完成扩容。
扩容条件:
①当存放Node
节点的数组长度小于64,并且单个哈希桶的链表的存放节点个数达到8的时候,会触发扩容;如果存放节点的数组长度>=64,那么会把当前的链表树化为红黑树。 ②当存储的实际key的数量/Node
数组的长度达到负载因子的时候,会触发扩容
以上两点,和jdk1.8版本的HashMap的扩容前提条件类似,没有太大的差别。
满足上面的条件之后,会进入到下面的操作:
首先申请一个原来数组大小2倍的新数组
如果有多个线程同时尝试扩容,那么,ConcurrentHashMap会对这些线程进行"分工”。何为分工呢?画个图简单看一看:
也就是,每一个线程,分别对原有的数组上面的元素分别进行"搬运"操作,让它们都被各自的线程重新哈希到新的数组上面的位置,这样的效率会更加的高。
同时,如果ConcurrentHashMap如果正在扩容的时候:
其他线程调用get方法,那么调用的线程会查询旧数组和新的数组当中是否存在对应的key。
其他线程如果调用remove方法,那么调用的线程会把旧数组和新的数组当中的key都删除掉。
而不是一直阻塞等待,直到扩容执行完毕。
你是否还在寻找稳定的海外服务器提供商?创新互联www.cdcxhl.cn海外机房具备T级流量清洗系统配攻击溯源,准确流量调度确保服务器高可用性,企业级服务器适合批量采购,新人活动首月15元起,快前往官网查看详情吧
我们在微信上24小时期待你的声音
解答本文疑问/技术咨询/运营咨询/技术建议/互联网交流