温馨提示×

温馨提示×

您好,登录后才能下订单哦!

密码登录×
登录注册×
其他方式登录
点击 登录注册 即表示同意《亿速云用户服务条款》

怎么理解并掌握ConcurrentHashMap

发布时间:2021-10-21 13:58:31 来源:亿速云 阅读:165 作者:iii 栏目:编程语言

本篇内容主要讲解“怎么理解并掌握ConcurrentHashMap”,感兴趣的朋友不妨来看看。本文介绍的方法操作简单快捷,实用性强。下面就让小编来带大家学习“怎么理解并掌握ConcurrentHashMap”吧!

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)时间复杂度下,查找目标元素。我们直观的看一个图:

怎么理解并掌握ConcurrentHashMap

另外你还记得我们上一篇举的示例吗?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元素存放的位置

我们来直观的看一个图:

怎么理解并掌握ConcurrentHashMap

前一个元素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冲突的多个元素连接起来,形成一个链表

我们直观的看一个图:

怎么理解并掌握ConcurrentHashMap

你看,这就是拉链法。

关于二者适用场景

现在你已经知道了什么是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方法:

怎么理解并掌握ConcurrentHashMap

我们说直接在方法上加synchronized关键字,实现线程安全有什么问题呢?最大的问题就是锁粒度太大,导致并发性能低,不足以应用在高并发业务场景。这也是为什么Hashtable出身以来,从未受宠的原因,你也不喜欢它对吧!千万别说喜欢,非要喜欢的话怎么不见在你的项目中使用Hashtable呢?

说了这么多别人的不是,其目的都是为了衬托ConcurrentHashMap的主角光环。那你说说看吧,ConcurrentHashMap到底是如何实现线程安全,又是如何支持高并发的?我们从两个方面来看。

既然要线程安全,那么锁,肯定是要锁的,基础原理不变

另外要支持高并发业务场景,都加锁了,还怎么实现高并发呢?这个地方你需要特别留意了,这里我将给你分享一个解决大、且复杂问题的通用思想,我们说:面对大的,复杂的业务问题,要想实现化繁为简,唯一的手段即是拆分。今天我们说分布式,微服务化其核心都是拆字决!

那具体到ConcurrentHashMap中,它到底是如何拆的呢?它是这么拆的:通过分段锁,即保障了线程安全,又提升了并发能力

关于分段锁,你可以这么去理解:原来是一个大锁,限制了并发能力,因为只有一把锁;现在我们把大锁分成多把小锁(ConcurrentHashMap默认是16个分段锁),可以同时支持16个并发

好了,文字分析我们差不多讲明白了,接下来我通过源码,以及画一个图,让你更好的理解ConcurrentHashMap(你需要注意,我当前的jdk版本是7)。

ConcurrentHashMap图示:

怎么理解并掌握ConcurrentHashMap

ConcurrentHashMap源码代表:

通过上图我们直观看到在jdk7中,ConcurrentHashMap它是通过分段锁实现支持高并发,默认情况下,有16个分段锁,其中每一个分段锁中,即是一个HashMap。

接下来我们一起通过源码,辅助理解上图。

  • 底层数据结构,数组

/**
* The segments, each of which is a specialized hash table.
*/
final Segment<K,V>[] 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 Segment<K,V> extends ReentrantLock implements Serializable {
     ......   
}
  • 分段锁内部定义

/*
*每个Segment,都是一个HashMap
*/
transient volatile HashEntry<K,V>[] 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”有了更深的了解,不妨来实际操作一番吧!这里是亿速云网站,更多相关内容可以进入相关频道进行查询,关注我们,继续学习!

向AI问一下细节

免责声明:本站发布的内容(图片、视频和文字)以原创、转载和分享为主,文章观点不代表本网站立场,如果涉及侵权请联系站长邮箱:is@yisu.com进行举报,并提供相关证据,一经查实,将立刻删除涉嫌侵权内容。

AI