高级并发编程系列十六(一文搞懂ConcurrentHashMap)

1.考考你

早上好!今天我要跟你分享的是ConcurrentHashMapjava

尽管你说大家的项目业务复杂度不高,没有多少用户量,不须要考虑并发状况,你历来都只用到了HashMap,不关心ConcurrentHashMap。那也没有关系,ConcurrentHashMap与HashMap师出同门,有着千丝万缕的关系,它们两者的武功路数是同样的,只不过ConcurrentHashMap的修为要更高(它是并发安全的HashMap)。编程

所以借助上一篇咱们刚分享完HashMap,趁热打铁一起把ConcurrentHashMap一块儿收拾了。那么这一篇咱们就接着上一篇,主要搞清楚这么几个问题:数组

  • 上一篇咱们分析了HashMap的底层实现原理,好比说底层数据结构是数组,经过拉链法解决hash冲突等。那你能告诉我,在实际项目中该如何更好的使用HashMap吗?缓存

  • 上一篇咱们分析了HashMap在解决hash冲突的时候,有两种方案:开放寻址法、拉链法。那你能告诉我,它们之间有什么区别吗?安全

  • 你说ConcurrentHashMap是线程安全版本的HashMap。那么你能告诉我,它是如何实现线程安全的?在jdk8与jdk8之前版本中,有什么差别吗?以及jdk8中改变线程安全实现方式的背后逻辑吗?(灵魂三拷问......)数据结构

好了,带上以上几个问题,让咱们开始吧并发

2.案例

2.1.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;

2.2.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冲突:开放寻址法、拉链法

2.2.1.关于开放寻址法

开放寻址法,是指当发生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)的位置。

你看,这就是开放寻址法

2.2.2.关于拉链法

拉链法,是指当发生hash冲突后,好比说某个key,经过哈希函数hash(key),指向了数组下标5的位置。此时不巧下标5的位置已经存放了元素,即发生了hash冲突。

那么拉链法的作法,不一样于开放寻址法。它不须要从下标5的位置向后搜索,它是直接定位到下标5的位置,在此处经过链表,将发生hash冲突的多个元素链接起来,造成一个链表

咱们直观的看一个图:

你看,这就是拉链法。

2.2.3.关于两者适用场景

如今你已经知道了什么是hash冲突,以及hash冲突的两种主要解决方案:开放寻址法、拉链法

咱们再来探讨一个问题,什么场景下适合用开放寻址法?什么场景下适合用拉链法呢?

咱们知道开放寻址法,最大的特色就是当发生hash冲突的时候,有向后搜索的操做。那么假设在存放大量目标元素对象的场景下,发生冲突的几率会很是大,每次发生冲突,都要向后搜索操做,会比较影响性能。

所以,开放寻址法适合在容器容量需求不大(即目标元素很少),hash冲突发生几率小的场景下,我建议你能够看一下ThreadLocalMap源码,ThreadLocalMap即便用了开放寻址法解决hash冲突。

知道了开放寻址法的适用场景后,咱们经过反向思考,即不难理解拉链法的使用场景了。拉链法适合在目标元素多,容器容量需求大、hash冲突发生几率大的业务场景。不用说,你已经知道了,咱们一直的主角HashMap,ConcurrentHashMap都使用了拉链法解决hash冲突。

2.3.ConcurrentHashMap详解

为了方便你理解ConcurrentHashMap,咱们前面作了很是长的铺垫,上一篇文章以及这篇文章的上半部分。

如今相信你已经理解了HashMap,那咱们就开始进入ConcurrentHashMap的内容了。关于ConcurrentHashMap,大方向上你先有一个印象:ConcurrentHashMap它是HashMap的线程安全版本,它与HashMap一脉相传,是师兄弟关系,只不过它是关门弟子,得了师傅的真传,能力要更增强大一些

上面这段话的意思,大体是想要告诉你,ConcurrentHashMap的底层实现原理,用了什么数据结构,如何解决hash冲突等都与HashMap同样。咱们只须要关心它是如何实现线程安全的就能够了。

那就让咱们开始吧,你须要注意一下,ConcurrentHashMap线程安全的实现,在jdk8版本,与jdk8之前的版本区别比较大,咱们分开来讲

2.3.1.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<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操做。话很少说,咱们看图:

相关文章
相关标签/搜索