ConcurrentHashMap 是 HashMap 的多线程版本,HashMap 在并发操作时会有各种问题,比如死循环问题、数据覆盖等问题。而这些问题,只要使用 ConcurrentHashMap 就可以完美解决了,那问题来了,ConcurrentHashMap 是如何保证线程安全的?它的底层又是如何实现的?接下来我们一起来看。

JDK 1.7 底层实现

ConcurrentHashMap 在不同的 JDK 版本中实现是不同的,在 JDK 1.7 中它使用的是数组加链表的形式实现的,而数组又分为:大数组 Segment 和小数组 HashEntry。 大数组 Segment 可以理解为 MySQL 中的数据库,而每个数据库(Segment)中又有很多张表 HashEntry,每个 HashEntry 中又有多条数据,这些数据是用链表连接的,如下图所示:

JDK 1.7 线程安全实现

了解了 ConcurrentHashMap 的底层实现,再看它的线程安全实现就比较简单了。

接下来,我们通过添加元素 put 方法,来看 JDK 1.7 中 ConcurrentHashMap 是如何保证线程安全的,具体实现源码如下:

final V put(K key, int hash, V value, boolean onlyIfAbsent) {
// 在往该 Segment 写入前,先确保获取到锁
HashEntry<K,V> node = tryLock() ? null : scanAndLockForPut(key, hash, value);
V oldValue;
try {
// Segment 内部数组
HashEntry<K,V>[] tab = table;
int index = (tab.length - 1) & hash;
HashEntry<K,V> first = entryAt(tab, index);
for (HashEntry<K,V> e = first;;) {
if (e != null) {
K k;
// 更新已有值...
}
else {
// 放置 HashEntry 到特定位置,如果超过阈值则进行 rehash
// 忽略其他代码...
}
}
} finally {
// 释放锁
unlock();
}
return oldValue;
}

从上述源码我们可以看出,Segment 本身是基于 ReentrantLock 实现的加锁和释放锁的操作,这样就能保证多个线程同时访问 ConcurrentHashMap 时,同一时间只有一个线程能操作相应的节点,这样就保证了 ConcurrentHashMap 的线程安全了。

也就是说 ConcurrentHashMap 的线程安全是建立在 Segment 加锁的基础上的,所以我们把它称之为分段锁或片段锁,如下图所示:

JDK 1.8 底层实现

在 JDK 1.7 中,ConcurrentHashMap 虽然是线程安全的,但因为它的底层实现是数组 + 链表的形式,所以在数据比较多的情况下访问是很慢的,因为要遍历整个链表,而 JDK 1.8 则使用了数组 + 链表/红黑树的方式优化了 ConcurrentHashMap 的实现,具体实现结构如下:



链表升级为红黑树的规则:当链表长度大于 8,并且数组的长度大于 64 时,链表就会升级为红黑树的结构。

PS:ConcurrentHashMap 在 JDK 1.8 虽然保留了 Segment 的定义,但这仅仅是为了保证序列化时的兼容性,不再有任何结构上的用处了。

JDK 1.8 线程安全实现

在 JDK 1.8 中 ConcurrentHashMap 使用的是 CAS + volatile 或 synchronized 的方式来保证线程安全的,它的核心实现源码如下:

final V putVal(K key, V value, boolean onlyIfAbsent) { if (key == null || value == null) throw new NullPointerException();
int hash = spread(key.hashCode());
int binCount = 0;
for (Node<K,V>[] tab = table;;) {
Node<K,V> f; int n, i, fh; K fk; V fv;
if (tab == null || (n = tab.length) == 0)
tab = initTable();
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) { // 节点为空
// 利用 CAS 去进行无锁线程安全操作,如果 bin 是空的
if (casTabAt(tab, i, null, new Node<K,V>(hash, key, value)))
break;
}
else if ((fh = f.hash) == MOVED)
tab = helpTransfer(tab, f);
else if (onlyIfAbsent
&& fh == hash
&& ((fk = f.key) == key || (fk != null && key.equals(fk)))
&& (fv = f.val) != null)
return fv;
else {
V oldVal = null;
synchronized (f) {
// 细粒度的同步修改操作...
}
}
// 如果超过阈值,升级为红黑树
if (binCount != 0) {
if (binCount >= TREEIFY_THRESHOLD)
treeifyBin(tab, i);
if (oldVal != null)
return oldVal;
break;
}
}
}
addCount(1L, binCount);
return null;
}

从上述源码可以看出,在 JDK 1.8 中,添加元素时首先会判断容器是否为空,如果为空则使用 volatile 加 CAS 来初始化。如果容器不为空则根据存储的元素计算该位置是否为空,如果为空则利用 CAS 设置该节点;如果不为空则使用 synchronize 加锁,遍历桶中的数据,替换或新增节点到桶中,最后再判断是否需要转为红黑树,这样就能保证并发访问时的线程安全了。

我们把上述流程简化一下,我们可以简单的认为在 JDK 1.8 中,ConcurrentHashMap 是在头节点加锁来保证线程安全的,锁的粒度相比 Segment 来说更小了,发生冲突和加锁的频率降低了,并发操作的性能就提高了。而且 JDK 1.8 使用的是红黑树优化了之前的固定链表,那么当数据量比较大的时候,查询性能也得到了很大的提升,从之前的 O(n) 优化到了 O(logn) 的时间复杂度,具体加锁示意图如下:

总结

ConcurrentHashMap 在 JDK 1.7 时使用的是数据加链表的形式实现的,其中数组分为两类:大数组 Segment 和小数组 HashEntry,而加锁是通过给 Segment 添加 ReentrantLock 锁来实现线程安全的。而 JDK 1.8 中 ConcurrentHashMap 使用的是数组+链表/红黑树的方式实现的,它是通过 CAS 或 synchronized 来实现线程安全的,并且它的锁粒度更小,查询性能也更高。

是非审之于己,毁誉听之于人,得失安之于数。

公众号:Java面试真题解析

文章合集:https://gitee.com/mydb/interview

为什么ConcurrentHashMap是线程安全的?的更多相关文章

  1. HashMap(不是线程安全)与ConcurrentHashMap(线程安全)

    HashMap不是线程安全的 ConcurrentHashMap是线程安全的 从JDK1.2起,就有了HashMap,正如前一篇文章所说,HashMap不是线程安全的,因此多线程操作时需要格外小心. ...

  2. 面试连环炮系列(九):为什么ConcurrentHashMap是线程安全的

    为什么ConcurrentHashMap是线程安全的 JDK1.7中,ConcurrentHashMap使用的锁分段技术,将数据分成一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一 ...

  3. 使用ConcurrentHashMap一定线程安全?

    前言 老王为何半夜惨叫?几行代码为何导致服务器爆炸?说好的线程安全为何还是出问题?让我们一起收看今天的<走进IT> 正文 CurrentHashMap出现背景 说到ConcurrentHa ...

  4. ConcurrentHashMap实现线程安全的原理

    并发环境下为什么使用ConcurrentHashMap 1. HashMap在高并发的环境下,执行put操作会导致HashMap的Entry链表形成环形数据结构,从而导致Entry的next节点始终不 ...

  5. 面试题:ConcurrentHashMap实现线程安全的原理

    在ConcurrentHashMap没有出现以前,jdk使用hashtable来实现线程安全,但是hashtable是将整个hash表锁住,所以效率很低下. ConcurrentHashMap将数据分 ...

  6. ConcurrentHashMap、synchronized与线程安全

    明明用了ConcurrentHashMap,可是始终线程不安全, 下面我们来看代码: public class Test40 { public static void main(String[] ar ...

  7. Java ConcurrentHashMap存入引用对象时也是线程安全的

    本人小白,看到资料说ConcurrentHashMap是线程安全的,get过程不需要加锁,put是线程安全的,推荐高并发时使用.但是本人不清楚是否该map中存入的引用类型对象,对象属性变化也是否线程安 ...

  8. ConcurrentHashMap并不是绝对线程安全的

    import java.util.HashMap; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; publi ...

  9. 【线程】结果缓存实现(future与concurrenthashmap)

    Computable<A,V>接口中生命了一个函数Computable,其输入类型为A,输出类型为V,在ExpensiveFunction中实现的Computable,需要很长时间来计算结 ...

随机推荐

  1. JAVA通过经纬度获取两点之间的距离

    private static double EARTH_RADIUS = 6378.137; private static double rad(double d) { return d * Math ...

  2. IDEA安装vue.js插件后,new没有Vue component

    首先要安装vue相关的插件vue.js 但是很多人安装vue.js右键发现没有vue Componment,解决方法如下 Settings>Editor>File and Code Tem ...

  3. 【九度OJ】题目1023:EXCEL排序 解题报告

    [九度OJ]题目1023:EXCEL排序 解题报告 标签(空格分隔): 九度OJ [LeetCode] http://ac.jobdu.com/problem.php?pid=1023 题目描述: E ...

  4. 4 种主流的 API 架构风格对比

    1RPC:调用另一个系统的函数 RPC 的工作机制 客户端调用一个远程的过程,将参数和附加信息序列化为消息,然后将消息发送到服务端.服务端在接受到消息后,将信息的内容反序列化,执行所请求的操作,然后将 ...

  5. 第十一个知识点:DLP,CDH和DDH问题都是什么?

    第十一个知识点:DLP,CDH和DDH问题都是什么 这是第11篇也是数学背景的第二篇.主要关注群操作如何被用于设计密码基础. 就像你现在知道的那样,密码学经常依赖于'难问题'.这也就是说,如果我们假设 ...

  6. 网站迁移纪实:从Web Form 到 Asp.Net Core (Abp vNext 自定义开发)

    问题和需求 从2004年上线,ZLDNN.COM运行已经超过16年了,一直使用DotNetNuke平台(现在叫DNN Platform),从最初的DotNetNuke 2.1到现在使用的7.4.先是在 ...

  7. C++string字符串截取其中元素 截取定位字符串

    #include <iostream> #include <string> using namespace std; /** * 截取str后的元素 * @param stre ...

  8. X86系统或intel RK主板上EDP转LVDS屏转接板|CS5211DP转LVDS设计

    众所周知LVDS接口是美国NS美国国家半导体公司为克服以TTL电平方式传输宽带高码率数据时功耗大,电磁干扰大等缺点而研制的一种数字视频信号传输方式.由于其采用低压和低电流驱动方式,实现了低噪声和低功耗 ...

  9. ARTS Week 19

    Algorithm 本周的 LeetCode 题目为 5. 最长回文子串 给你一个字符串 s,找到 s 中最长的回文子串.例如输入:s = "babad",输出:"bab ...

  10. Android 摄像头预览悬浮窗,可拖动,可显示在其他app上方

    市面上常见的摄像头悬浮窗,如微信.手机QQ的视频通话功能,有如下特点: 整屏页面能切换到一个小的悬浮窗 悬浮窗能运行在其他app上方 悬浮窗能跳回整屏页面,并且悬浮窗消失 我们探讨过用CameraX打 ...