jdk1.8的HashMap和ConcurrentHashMap

in 编程
关注公众号【好便宜】( ID:haopianyi222 ),领红包啦~
阿里云,国内最大的云服务商,注册就送数千元优惠券:https://t.cn/AiQe5A0g
腾讯云,良心云,价格优惠: https://t.cn/AieHwwKl
搬瓦工,CN2 GIA 优质线路,搭梯子、海外建站推荐: https://t.cn/AieHwfX9

本文针对jdk1.8的ConcurrentHashMap

1 1.8的HashMap设计

1.1 整体概览

HashMap采用的是数组+链表+红黑树的形式。

数组是可以扩容的,链表也是转化为红黑树的,这2种方式都可以承载更多的数据。

用户可以设置的参数:初始总容量默认16,默认的加载因子0.75

初始的数组个数默认是16
容量X加载因子=阈值

一旦目前容量超过该阈值,则执行扩容操作。

什么时候扩容?

什么时候链表转化为红黑树?(上面已经提到了)

目前形象的表示数组中的一个元素称为一个桶

1.2 put过程

源码如下:

HashMap的put过程

下面来说说这个扩容的过程

1.3 扩容过程

按照2倍扩容的方式,那么就需要将之前的所有元素全部重新按照2倍桶的长度重新计算所在桶。这里为啥是2倍?

因为2倍的话,更加容易计算他们所在的桶,并且各自不会相互干扰。如原桶长度是4,现在桶长度是8,那么

为啥是这样呢?

桶0中的元素的hash值后2位必然是00,这些hash值可以根据后3位000或者100分成2类数据。他们分别&(8-1)即&111,则后3位为000的在桶0中,后3位为100的必然在桶4中。其他同理,也就是说桶4和桶0重新瓜分了原来桶0中的元素。

如果换成其他倍数,那么瓜分就比较混乱了。

这样在瓜分这些数据的时候,只需要先把这些数据分类,如上述桶0中分成000和100 2类,然后直接构成新的链表,分类完毕后,直接将新的链表挂在对应的桶下即可,源码如下:

HashMap的扩容过程

上述 (e.hash & oldCap) == 0 即可将原桶中的数据分成2类

上述是对于链表情况下的重新移动,而针对红黑树情况下:

则需要考虑分类之后是否还需要依然保持红黑树,如果个数少则直接使用链表即可。

1.4 get过程

get过程比较简单

源码如下:

HashMap的get过程

2 1.7的ConcurrentHashMap设计

ConcurrentHashMap是线程安全,通过分段锁的方式提高了并发度。分段是一开始就确定的了,后期不能再进行扩容的。

其中的段Segment继承了重入锁ReentrantLock,有了锁的功能,同时含有类似HashMap中的数组加链表结构(这里没有使用红黑树)

虽然Segment的个数是不能扩容的,但是单个Segment里面的数组是可以扩容的。

2.1 整体概览

ConcurrentHashMap有3个参数:

然后我们需要知道的是:

所以默认情况下,segment的个数sszie=16,每个segment的初始容量cap=2,单个segment的阈值threshold=1

2.2 put过程

整体代码逻辑见如下源码:

1.7ConcurrentHashMap的put过程1

其中上述Segment的put过程源码如下:

1.7ConcurrentHashMap的put过程2

2.3 扩容过程

这个扩容是在Segment的锁的保护下进行扩容的,不需要关注并发问题。

Segment扩容如下所示

这里的重点就是:

首先找到一个lastRun,lastRun之后的元素和lastRun是在同一个桶中,所以后面的不需要进行变动。

然后对开始到lastRun部分的元素,重新计算下设置到newTable中,每次都是将当前元素作为newTable的首元素,之前老的链表作为该首元素的next部分。

2.4 get过程

源码如下:

1.7ConcurrentHashMap的get过程

3 1.8的ConcurrentHashMap设计

1.8的ConcurrentHashMap摒弃了1.7的segment设计,而是在1.8HashMap的基础上实现了线程安全的版本,即也是采用数组+链表+红黑树的形式。

数组可以扩容,链表可以转化为红黑树

3.1 整体概览

有一个重要的参数sizeCtl,代表数组的大小(但是还有其他取值及其含义,后面再详细说到)

用户可以设置一个初始容量initialCapacity给ConcurrentHashMap

sizeCtl=大于(1.5倍initialCapacity+1)的最小的2的幂次。

即initialCapacity=20,则sizeCtl=32,如initialCapacity=24,则sizeCtl=64。

初始化的时候,会按照sizeCtl的大小创建出对应大小的数组

3.2 put过程

源码如下所示:

1.8ConcurrentHashMap的put过程

下面就来重点看看这个扩容过程

3.3 扩容过程

一旦链表中的元素个数超过了8个,那么可以执行数组扩容或者链表转为红黑树,这里依据的策略跟HashMap依据的策略是一致的。

当数组长度还未达到64个时,优先数组的扩容,否则选择链表转为红黑树。

源码如下所示:

扩容或者链表转红黑树

重点来看看这个扩容过程,即看下上述tryPresize方法,也可以看到上述是2倍扩容的方式

输入图片说明

第一个执行的线程会首先设置sizeCtl属性为一个负值,然后执行transfer(tab, null),其他晚进来的线程会检查当前扩容是否已经完成,没完成则帮助进行扩容,完成了则直接退出。

该ConcurrentHashMap的扩容操作可以允许多个线程并发执行,那么就要处理好任务的分配工作。每个线程获取一部分桶的迁移任务,如果当前线程的任务完成,查看是否还有未迁移的桶,若有则继续领取任务执行,若没有则退出。在退出时需要检查是否还有其他线程在参与迁移工作,如果有则自己什么也不做直接退出,如果没有了则执行最终的收尾工作。

下面来看下详细的代码:

扩容代码

3.4 get过程

详细代码如下

1.8ConcurrentHashMap的get过程

至此,ConcurrentHashMap主要的操作都粗略的介绍完毕了,其他一些操作靠各位自行去看了。

下面针对一些问题来进行解答

4 问题分析

4.1 ConcurrentHashMap读为什么不需要锁?

我们通常使用读写锁来保护对一堆数据的读写操作。读时加读锁,写时加写锁。在什么样的情况下可以不需要读锁呢?

如果对数据的读写是一个原子操作,那么此时是可以不需要读锁的。如ConcurrentHashMap对数据的读写,写操作是不需要分2次写的(没有中间状态),读操作也是不需要2次读取的。假如一个写操作需要分多次写,必然会有中间状态,如果读不加锁,那么可能就会读到中间状态,那就不对了。

假如ConcurrentHashMap提供put(key1,value1,key2,value2),写入的时候必然会存在中间状态即key1写完成,但是key2还未写,此时如果读不加锁,那么就可能读到key1是新数据而key2是老数据的中间状态。

虽然ConcurrentHashMap的读不需要锁,但是需要保证能读到最新数据,所以必须加volatile。即数组的引用需要加volatile,同时一个Node节点中的val和next属性也必须要加volatile。

4.2 ConcurrentHashMap是否可以在无锁的情况下进行迁移?

目前1.8的ConcurrentHashMap迁移是在锁定旧桶的前提下进行迁移的,然而并没有去锁定新桶。那么就可能提出如下问题:

从上面看到我们在迁移的时候还是需要对旧桶锁定的,能否在无锁的情况下实现迁移?

可以参考参考这篇论文Split-Ordered Lists: Lock-Free Extensible Hash Tables

一旦扩容就涉及到迁移桶中元素的操作,将一个桶中的元素迁移到另一个桶中的操作不是一个原子操作,所以需要在锁的保护下进行迁移。如果扩容操作是移动桶的指向,那么就可以通过一个CAS操作来完成扩容操作。上述Split-Ordered Lists就是把所有元素按照一定的顺序进行排列。该list被分成一段一段的,每一段都代表某个桶中的所有元素。每个桶中都有一个指向第一个元素的指针,如下图结构所示:

Split-Ordered Lists

每一段其实也是分成2类的,如同前面所说的HashMap在扩容是分成2类的情况是一样的,此时Split-Ordered Lists在扩容时就只需要将新桶的指针指向这2类的分界点即可。

这一块之后再详细说明吧。

4.3 ConcurrentHashMap曾经的弱一致性

具体详见这篇针对老版本的ConcurrentHashMap的说明文章为什么ConcurrentHashMap是弱一致的

文中已经解释到:对数组的引用是volatile来修饰的,但是数组中的元素并不是。即读取数组的引用总是能读取到最新的值,但是读取数组中某一个元素的时候并不一定能读到最新的值。所以说是弱一致性的。

我觉得这个只需要稍微改动下就可以实现强一致性:

但是现在1.7版本的ConcurrentHashMap对于数组中元素的写也是加了volatile的,如下代码

1.7版本的ConcurrentHashMap对于数组元素的写

1.8的方式就是:直接将新加入的元素写入next属性(含有volatile修饰)中而不是修改桶中的第一个元素。

1.8版本的ConcurrentHashMap的volatile写

所以在1.7和1.8版本的ConcurrentHashMap中不再是弱一致性,写入的数据是可以立马本读到的。

5 结束语

本篇文章因为涉及的话题众多,难免有疏漏之处,还请指出纠正。更多的问题可以加群讨论,详见这篇加群说明文章加群指南

欢迎继续来讨论,越辩越清晰

欢迎关注微信公众号:乒乓狂魔

乒乓狂魔微信公众号

关注公众号【好便宜】( ID:haopianyi222 ),领红包啦~
阿里云,国内最大的云服务商,注册就送数千元优惠券:https://t.cn/AiQe5A0g
腾讯云,良心云,价格优惠: https://t.cn/AieHwwKl
搬瓦工,CN2 GIA 优质线路,搭梯子、海外建站推荐: https://t.cn/AieHwfX9
扫一扫关注公众号添加购物返利助手,领红包
Comments are closed.

推荐使用阿里云服务器

超多优惠券

服务器最低一折,一年不到100!

朕已阅去看看