本篇内容主要讲解“怎么理解并掌握ConcurrentHashMap”,感兴趣的朋友不妨来看看。本文介绍的方法操作简单快捷,实用性强。下面就让小编来带大家学习“怎么理解并掌握ConcurrentHashMap”吧!
创新互联公司制作网站网页找三站合一网站制作公司,专注于网页设计,成都网站制作、成都网站设计、外贸营销网站建设,网站设计,企业网站搭建,网站开发,建网站业务,680元做网站,已为上千多家服务,创新互联公司网站建设将一如既往的为我们的客户提供最优质的网站建设、网络营销推广服务!
HashMap最佳实践
现在我们知道了,在实际项目中,我们是把HashMap作为容器来使用的。既然是容器,那就需要考虑这么几个问题:
容器的容量大小,能够支持存放多少个元素,一开始给多少合适呢?(初始容量问题)
指定了容器初始容量大小后,万一元素太多,容器放不下了如何处理呢?(容器扩容、装载因子问题)
针对上面的问题,我们来分析一下:
在HashMap中,默认的初始容量大小是16, 在实际项目中,我们可以考虑预估要存入的元素个数,根据元素个数设置合适的初始容量。减少HashMap动态扩容,减少重建哈希表,从而提升性能
/** * The default initial capacity - MUST be a power of two. * 默认初始容量,HashMap的容量最好是保持 2的n次方 */ static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
HashMap装载因子,默认是0.75。表示在HashMap中,当元素的个数超过:capacity * 0.75的时候,就会启动动态扩容,每次扩容后容量大小都是之前的两倍
装载因子越大,表示空闲空间越小,对应的HashMap冲突的概率就会越大。在实际项目中,我们可以设置合适的装载因子,提升HashMap性能
/** * The load factor used when none specified in constructor. */ static final float DEFAULT_LOAD_FACTOR = 0.75f;
hash冲突详解
现在你已经知道了在项目中用好HashMap,需要考虑的一些问题:初始容量、装载因子等。
接下来我们一起来看另外一个问题:如何解决hash冲突。关于hash冲突,单从应用HashMap来说,我们并不需要关心,毕竟大多数时候,我们都仅仅是使用HashMap,并不会考虑从0到1写一个HashMap。但是我还是想建议你了解一下,关于整个世界的认知,我们都应该知其然,且知其所以然。
上一篇我们提到关于hash冲突,主要有两种解决方案:开放寻址法,拉连法。但是当时我并没有详细说明,我们跳过了,现在我们一起来看一下,什么是开放寻址法?什么是拉链法?
我们知道HashMap的底层数据结构是数组,数组的容量是有限的(我们暂时不考虑扩容,因为扩容后容量也还是有限的,只是比起扩容前大一倍)。
我们也知道HashMap的存储是key/value键值对,需要将任意类型的key,通过散列函数hash(key),转换成数组下标,与数组联系起来,实现在O(1)时间复杂度下,查找目标元素。我们直观的看一个图:
另外你还记得我们上一篇举的示例吗?hash(0+5)=5,hash(1+4)=5,hash(2+3)=5。假设当前目标数组下标是:5,那你也看到了,左右key:0+5,1+4,2+3并不相同,但是通过hash函数后,却都指向了数组下标:5的位置。这就是hash冲突的由来。
好了,我又带着你回顾了一遍hash冲突,现在我们重新回到解决hash冲突:开放寻址法、拉链法。
关于开放寻址法
开放寻址法,是指当发生hash冲突后,比如说某个key,通过哈希函数hash(key),指向了数组下标5的位置。此时不巧下标5的位置已经存放了元素,即发生了hash冲突。
那么开放寻址法的做法,是从数组下标5的位置开始向后搜索,寻找到第一个空的,还未存放任何元素的下标位置,比如:8,作为当前key元素存放的位置。
我们来直观的看一个图:
前一个元素hash(1+4)=5,占用了数组下标5的位置;
后一个元素hash(2+3)=5,虽然指向了数组下标5位置,但是此时下标5的位置已经被hash(1+4)元素占用,所以hash(2+3)元素只能继续向后搜索,直到搜索到下标8的位置,发现下标8位置未使用,即作为元素hash(2+3)的位置。
你看,这就是开放寻址法。
关于拉链法
拉链法,是指当发生hash冲突后,比如说某个key,通过哈希函数hash(key),指向了数组下标5的位置。此时不巧下标5的位置已经存放了元素,即发生了hash冲突。
那么拉链法的做法,不同于开放寻址法。它不需要从下标5的位置向后搜索,它是直接定位到下标5的位置,在此处通过链表,将发生hash冲突的多个元素连接起来,形成一个链表。
我们直观的看一个图:
你看,这就是拉链法。
关于二者适用场景
现在你已经知道了什么是hash冲突,以及hash冲突的两种主要解决方案:开放寻址法、拉链法。
我们再来探讨一个问题,什么场景下适合用开放寻址法?什么场景下适合用拉链法呢?
我们知道开放寻址法,最大的特点就是当发生hash冲突的时候,有向后搜索的操作。那么假设在存放大量目标元素对象的场景下,发生冲突的概率会非常大,每次发生冲突,都要向后搜索操作,会比较影响性能。
因此,开放寻址法适合在容器容量需求不大(即目标元素不多),hash冲突发生概率小的场景下,我建议你可以看一下ThreadLocalMap源码,ThreadLocalMap即使用了开放寻址法解决hash冲突。
知道了开放寻址法的适用场景后,我们通过反向思考,即不难理解拉链法的使用场景了。拉链法适合在目标元素多,容器容量需求大、hash冲突发生概率大的业务场景。不用说,你已经知道了,我们一直的主角HashMap,ConcurrentHashMap都使用了拉链法解决hash冲突。
ConcurrentHashMap详解
为了方便你理解ConcurrentHashMap,我们前面做了非常长的铺垫,上一篇文章以及这篇文章的上半部分。
现在相信你已经理解了HashMap,那我们就开始进入ConcurrentHashMap的内容了。关于ConcurrentHashMap,大方向上你先有一个印象:ConcurrentHashMap它是HashMap的线程安全版本,它与HashMap一脉相传,是师兄弟关系,只不过它是关门弟子,得了师傅的真传,能力要更加强大一些。
上面这段话的意思,大致是想要告诉你,ConcurrentHashMap的底层实现原理,用了什么数据结构,如何解决hash冲突等都与HashMap一样。我们只需要关心它是如何实现线程安全的就可以了。
那就让我们开始吧,你需要注意一下,ConcurrentHashMap线程安全的实现,在jdk8版本,与jdk8以前的版本区别比较大,我们分开来说。
jdk7版本的ConcurrentHashMap
我们先来看ConcurrentHashMap在jdk8以前版本的实现,以下我的分析,和涉及到的源码都是参考jdk7,你先留意一下。
谈到线程安全,你肯定想到了,除了加锁没有别的手段,并且你还进一步想到了我们在锁小节分享的:synchronized、或者Lock对象。
这里我们初步的想法是没有任何问题的,想要实现线程安全:加锁。但是我们还需要稍微往前思考一个问题:如果只是简单的加锁,那不就是Hashtable了吗?java设计者的大神们,你们是不是闲着没事干,顺便多写了一个ConcurrentHashMap呢?
答案肯定不是的,大神之所以称之为大神,其中有一个区别于常人的特质,就是从来不做无用功!
那要这么说,我们就需要搞清楚有了Hashtable,为什么还需要一个ConcurrentHashMap?
我们先回顾一下,Hashtable是如何实现线程安全的,以及它存在什么问题?你还记得吗,前面我们在高级并发编程系列十四(并发集合基础)一篇,分享了Hashtable实现线程安全的方式,它是直接在每个操作方法上加了synchronized关键字。比如下图,是我们熟悉的get方法:
我们说直接在方法上加synchronized关键字,实现线程安全有什么问题呢?最大的问题就是锁粒度太大,导致并发性能低,不足以应用在高并发业务场景。这也是为什么Hashtable出身以来,从未受宠的原因,你也不喜欢它对吧!千万别说喜欢,非要喜欢的话怎么不见在你的项目中使用Hashtable呢?
说了这么多别人的不是,其目的都是为了衬托ConcurrentHashMap的主角光环。那你说说看吧,ConcurrentHashMap到底是如何实现线程安全,又是如何支持高并发的?我们从两个方面来看。
既然要线程安全,那么锁,肯定是要锁的,基础原理不变
另外要支持高并发业务场景,都加锁了,还怎么实现高并发呢?这个地方你需要特别留意了,这里我将给你分享一个解决大、且复杂问题的通用思想,我们说:面对大的,复杂的业务问题,要想实现化繁为简,唯一的手段即是拆分。今天我们说分布式,微服务化其核心都是拆字决!
那具体到ConcurrentHashMap中,它到底是如何拆的呢?它是这么拆的:通过分段锁,即保障了线程安全,又提升了并发能力。
关于分段锁,你可以这么去理解:原来是一个大锁,限制了并发能力,因为只有一把锁;现在我们把大锁分成多把小锁(ConcurrentHashMap默认是16个分段锁),可以同时支持16个并发。
好了,文字分析我们差不多讲明白了,接下来我通过源码,以及画一个图,让你更好的理解ConcurrentHashMap(你需要注意,我当前的jdk版本是7)。
ConcurrentHashMap图示:
ConcurrentHashMap源码代表:
通过上图我们直观看到在jdk7中,ConcurrentHashMap它是通过分段锁实现支持高并发,默认情况下,有16个分段锁,其中每一个分段锁中,即是一个HashMap。
接下来我们一起通过源码,辅助理解上图。
底层数据结构,数组
/** * The segments, each of which is a specialized hash table. */ final Segment[] segments;
分段锁Segment定义
/** * Segments are specialized versions of hash tables. This * subclasses from ReentrantLock opportunistically, just to * simplify some locking and avoid separate construction. * 每个Segment,原来就是一个ReentrantLock,好熟悉有没有 */ static final class Segmentextends ReentrantLock implements Serializable { ...... }
分段锁内部定义
/* *每个Segment,都是一个HashMap */ transient volatile HashEntry[] table;
2.3.2.jdk8版本的ConcurrentHashMap
现在你已经知道了jdk7中的ConcurrentHashMap,我们说在jdk8中,它不再是分段锁的设计思想了,它变了!
变成什么了呢?变成了cas + synchronized组合来保障线程安全,同时实现支持高并发。这里你还记得什么是cas吗,如果不记得了,我推荐你看我这个系列的另外一篇文章:高级并发编程系列十二(一文搞懂cas)。
这里限于篇幅和侧重关注点,我就不再详细跟你说cas了,我只简单带你回顾一下cas的核心原理: cas本质上是不到黄河心不死,即不释放cpu,循环操作,直到操作成功为止。
它的操作原理是三个值:内存值A、期望值B、更新值C。每次操作都会比较内存值A,是否等于期望值B、如果等于则将内存值更新成值C,操作成功;如果内存值A,不等于期望值B,则操作失败,进行下一次循环操作。
给你回顾完cas,我们主要再来关注为什么在jdk8中,ConcurrentHashMap会通过cas +synchronized组合,来替换jdk7中的分段锁Segment呢?难道分段锁它不香吗?
我带着你一起分享一下我的个人理解:
我们知道cas是一种无锁化机制,大家都可以并行来抢占cpu(因为不加锁嘛),自然是你可以抢,我也可以抢
那要这么说,cas就非常适合并发冲突小,加锁临界点(范围)小的应用场景。
请说人话:什么是并发冲突小?简单说就是读多写少的业务场景,即读不需要加锁,写才需要加锁
嗯,你这么说我就明白了,我们在项目中使用HashMap,正好都是读多写少,一次写入,多次读取的业务场景。比如本地缓存实现方案
因此cas+synchronized组合实现ConcurrentHashMap的方案,在实际应用中,会比分段锁的实现方案,带来更高的并发支持,性能会更好!
你看,这么说,你是不是也能理解jdk8中的ConcurrentHashMap了。最后我们还是看一个图吧。
这个图你见过了,就是我们上一篇中HashMap的图。在jdk8中ConcurrentHashMap的底层数据结构上,与HashMap完全一样,它只是增加了cas+synchronized操作。话不多说,我们看图:
到此,相信大家对“怎么理解并掌握ConcurrentHashMap”有了更深的了解,不妨来实际操作一番吧!这里是创新互联网站,更多相关内容可以进入相关频道进行查询,关注我们,继续学习!
文章名称:怎么理解并掌握ConcurrentHashMap
本文来源:http://scgulin.cn/article/iijgso.html