面试必问之 ConcurrentHashMap 线程安全的具体实现方式(中)

简介: ConcurrentHashMap 是 Java 并发包中提供的一个线程安全且高效的 HashMap 实现,以弥补 HashMap 不适合在并发环境中操作使用的不足,本文就来分析下 ConcurrentHashMap 的实现原理,并对其实现原理进行分析!

上面介绍完了对象属性,我们继续来看看 ConcurrentHashMap 的构造方法,源码如下:118.jpg

this调用对应的构造方法,源码如下:

119.jpg从源码上可以看出,ConcurrentHashMap 初始化方法有三个参数,initialCapacity(初始化容量)为 16、loadFactor(负载因子)为 0.75、concurrentLevel(并发等级)为 16,如果不指定则会使用默认值。

其中,值得注意的是 concurrentLevel 这个参数,虽然 Segment 数组大小 ssize 是由 concurrentLevel 来决定的,但是却不一定等于 concurrentLevel,ssize 通过位移动运算,一定是大于或者等于 concurrentLevel 的最小的 2 的次幂!

通过计算可以看出,按默认的 initialCapacity 初始容量为 16,concurrentLevel 并发等级为 16,理论上就允许 16 个线程并发执行,并且每一个线程独占一把锁访问 Segment,不影响其它的 Segment 操作!

从之前的文章中,我们了解到 HashMap 在多线程环境下操作可能会导致程序死循环,仔细想想你会发现,造成这个问题无非是 put 和扩容阶段发生的!

那么这样我们就可以从 put 方法下手了,来看看 ConcurrentHashMap 是怎么操作的?

3.1、put 操作

ConcurrentHashMap 的 put 方法,源码如下:120.jpg

从源码可以看出,这部分的 put 操作主要分两步:

  • 定位 Segment 并确保定位的 Segment 已初始化;
  • 调用 Segment 的 put 方法;

真正插入元素的 put 方法,源码如下:121.jpg

从源码可以看出,真正的 put 操作主要分以下几步:

  • 第一步,尝试获取对象锁,如果获取到返回 true,否则执行scanAndLockForPut方法,这个方法也是尝试获取对象锁;
  • 第二步,获取到锁之后,类似 hashMap 的 put 方法,通过 key 计算所在 HashEntry 数组的下标;
  • 第三步,获取到数组下标之后遍历链表内容,通过 key 和 hash 值判断是否 key 已存在,如果已经存在,通过标识符判断是否覆盖,默认覆盖;
  • 第四步,如果不存在,采用头插法插入到 HashEntry 对象中;
  • 第五步,最后操作完整之后,释放对象锁;

我们再来看看,上面提到的scanAndLockForPut这个方法,源码如下:122.jpg

scanAndLockForPut这个方法,操作也是分以下几步:

  • 当前线程尝试去获得锁,查找 key 是否已经存在,如果不存在,就创建一个 HashEntry 对象;
  • 如果重试次数大于最大次数,就调用lock()方法获取对象锁,如果依然没有获取到,当前线程就阻塞,直到获取之后退出循环;
  • 在这个过程中,key 可能被别的线程给插入,所以在第 5 步中,如果 HashEntry 存储内容发生变化,重置重试次数;

通过scanAndLockForPut()方法,当前线程就可以在即使获取不到segment锁的情况下,完成需要添加节点的实例化工作,当获取锁后,就可以直接将该节点插入链表即可。

这个方法还实现了类似于自旋锁的功能,循环式的判断对象锁是否能够被成功获取,直到获取到锁才会退出循环,防止执行 put 操作的线程频繁阻塞,这些优化都提升了 put 操作的性能。

3.2、get 操作

get 方法就比较简单了,因为不涉及增、删、改操作,所以不存在并发故障问题,源码如下:123.jpg

由于 HashEntry 涉及到的共享变量都使用 volatile 修饰,volatile 可以保证内存可见性,所以不会读取到过期数据。

3.3、remove 操作

remove 操作和 put 方法差不多,都需要获取对象锁才能操作,通过 key 找到元素所在的 Segment 对象然后移除,源码如下:

124.jpg

与 get 方法类似,先获取 Segment 数组所在的 Segment 对象,然后通过 Segment 对象去移除元素,源码如下:

125.jpg

先获取对象锁,如果获取到之后执行移除操作,之后的操作类似 hashMap 的移除方法,步骤如下:

  • 先获取对象锁;
  • 计算 key 的 hash 值在 HashEntry[]中的角标;
  • 根据 index 角标获取 HashEntry 对象;
  • 循环遍历 HashEntry 对象,HashEntry 为单向链表结构;
  • 通过 key 和 hash 判断 key 是否存在,如果存在,就移除元素,并将需要移除的元素节点的下一个,向上移;
  • 最后就是释放对象锁,以便其他线程使用;

四、JDK1.8 中的 ConcurrentHashMap

虽然 JDK1.7 中的 ConcurrentHashMap 解决了 HashMap 并发的安全性,但是当冲突的链表过长时,在查询遍历的时候依然很慢!

在 JDK1.8 中,HashMap 引入了红黑二叉树设计,当冲突的链表长度大于 8 时,会将链表转化成红黑二叉树结构,红黑二叉树又被称为平衡二叉树,在查询效率方面,又大大的提高了不少。126.jpg

因为 HashMap 并不支持在多线程环境下使用, JDK1.8 中的 ConcurrentHashMap 和往期 JDK 中的 ConcurrentHashMa 一样支持并发操作,整体结构和 JDK1.8 中的 HashMap 类似,相比 JDK1.7 中的 ConcurrentHashMap, 它抛弃了原有的 Segment 分段锁实现,采用了 CAS + synchronized 来保证并发的安全性。

JDK1.8 中的 ConcurrentHashMap 对节点Node类中的共享变量,和 JDK1.7 一样,使用volatile关键字,保证多线程操作时,变量的可见行!127.jpg

其他的细节,与 JDK1.8 中的 HashMap 类似,我们来具体看看 put 方法!

4.1、put 操作

打开 JDK1.8 中的 ConcurrentHashMap 中的 put 方法,源码如下:

128.jpg

当进行 put 操作时,流程大概可以分如下几个步骤:

  • 首先会判断 key、value 是否为空,如果为空就抛异常!
  • 接着会判断容器数组是否为空,如果为空就初始化数组;
  • 进一步判断,要插入的元素f,在当前数组下标是否第一次插入,如果是就通过 CAS 方式插入;
  • 在接着判断f.hash == -1是否成立,如果成立,说明当前fForwardingNode节点,表示有其它线程正在扩容,则一起进行扩容操作;
  • 其他的情况,就是把新的Node节点按链表或红黑树的方式插入到合适的位置;
  • 节点插入完成之后,接着判断链表长度是否超过8,如果超过8个,就将链表转化为红黑树结构;
  • 最后,插入完成之后,进行扩容判断;

put 操作大致的流程,就是这样的,可以看的出,复杂程度比 JDK1.7 上了一个台阶。


相关文章
|
1月前
|
安全 Java
Qt经典面试题:Qt开启线程的几种方式
Qt经典面试题:Qt开启线程的几种方式
22 0
|
1月前
|
并行计算 安全 Java
C# .NET面试系列四:多线程
<h2>多线程 #### 1. 根据线程安全的相关知识,分析以下代码,当调用 test 方法时 i > 10 时是否会引起死锁? 并简要说明理由。 ```c# public void test(int i) { lock(this) { if (i > 10) { i--; test(i); } } } ``` 在给定的代码中,不会发生死锁。死锁通常是由于两个或多个线程互相等待对方释放锁而无法继续执行的情况。在这个代码中,只有一个线程持有锁,且没有其他线程参与,因此不
105 3
|
3月前
|
存储 缓存 并行计算
【面试问题】JDK并发类库提供的线程池实现有哪些?
【1月更文挑战第27天】【面试问题】JDK并发类库提供的线程池实现有哪些?
|
3月前
|
Java 调度 Windows
JAVA面试八股文之多线程基础知识
JAVA面试八股文之多线程基础知识
|
3月前
|
安全 Java Kotlin
面试必备:Kotlin 线程同步的 N 种方法
面试必备:Kotlin 线程同步的 N 种方法
82 0
|
3月前
|
调度
【面试问题】说说线程的生命周期?
【1月更文挑战第27天】【面试问题】说说线程的生命周期?
|
3月前
|
监控 安全 Java
【面试题】面试必备我跟面试官聊了一个小时线程池!
【面试题】面试必备我跟面试官聊了一个小时线程池!
|
2天前
|
Java 调度
Java面试必考题之线程的生命周期,结合源码,透彻讲解!
Java面试必考题之线程的生命周期,结合源码,透彻讲解!
23 1
|
2天前
|
Java
面试官让说出8种创建线程的方式,我只说了4种,然后挂了。。。
面试官让说出8种创建线程的方式,我只说了4种,然后挂了。。。
6 1
|
1月前
|
消息中间件 存储 算法
【C/C++ 泡沫精选面试题04】在实际项目中,多进程和多线程如何选择?
【C/C++ 泡沫精选面试题04】在实际项目中,多进程和多线程如何选择?
43 1