ConcurrentHashMap是如何保证线程安全

 更新时间:2022年01月24日 09:50:55   作者:磊哥聊编程  
大家好,本篇文章主要讲的是ConcurrentHashMap是如何保证线程安全,感兴趣的同学赶快来看一看吧,对你有帮助的话记得收藏一下

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

JDK 1.7 底层实现

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

为什么ConcurrentHashMap是线程安全的?_数组

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 加锁的基础上的,所以我们把它称之为分段锁或片段锁,如下图所示:

为什么ConcurrentHashMap是线程安全的?_数组_02

JDK 1.8 底层实现

在 JDK 1.7 中,ConcurrentHashMap 虽然是线程安全的,但因为它的底层实现是数组

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

为什么ConcurrentHashMap是线程安全的?_链表_03

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

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

JDK 1.8 线程安全实现

在 JDK 1.8 中 ConcurrentHashMap 使用的是 CAS

DK 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是线程安全的?_线程安全_04

总结

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

到此这篇关于ConcurrentHashMap是如何保证线程安全的文章就介绍到这了,更多相关ConcurrentHashMap线程安全内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • 使用resty Quartz执行定时任务的配置方法

    使用resty Quartz执行定时任务的配置方法

    这篇文章主要为大家介绍了使用resty Quartz来执行定时任务的配置方法,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步
    2022-03-03
  • Java 实现滑动时间窗口限流算法的代码

    Java 实现滑动时间窗口限流算法的代码

    这篇文章主要介绍了Java 实现滑动时间窗口限流算法的代码,本文通过实例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2020-11-11
  • 详解mybatis-plus实体类中字段和数据库中字段名不对应解决办法

    详解mybatis-plus实体类中字段和数据库中字段名不对应解决办法

    这篇文章主要介绍了详解mybatis-plus实体类中字段和数据库中字段名不对应解决办法,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2021-03-03
  • Java数组(Array)最全汇总(中篇)

    Java数组(Array)最全汇总(中篇)

    这篇文章主要介绍了Java数组(Array)最全汇总(中篇),本文章内容详细,通过案例可以更好的理解数组的相关知识,本模块分为了三部分,本次为中篇,需要的朋友可以参考下
    2023-01-01
  • Java日常练习题,每天进步一点点(15)

    Java日常练习题,每天进步一点点(15)

    下面小编就为大家带来一篇Java基础的几道练习题(分享)。小编觉得挺不错的,现在就分享给大家,也给大家做个参考。一起跟随小编过来看看吧,希望可以帮到你
    2021-07-07
  • Java构造器(构造方法)与方法区别说明

    Java构造器(构造方法)与方法区别说明

    这篇文章主要介绍了Java构造器(构造方法)与方法区别说明,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2020-09-09
  • Spring整合消息队列RabbitMQ流程

    Spring整合消息队列RabbitMQ流程

    Spring整合RabbitMQ很容易,但是整合的目的是为了使用,那要使用RabbitMQ就要对其有一定的了解,不然容易整成一团浆糊。因为说到底,Spring只是在封装RabbitMQ的API,让其更容易使用而已,废话不多说,让我们一起整它
    2023-03-03
  • springboot图片验证码功能模块

    springboot图片验证码功能模块

    用户登录几乎是一个线上系统必不可少且使用相对比较频繁的一个模块,为了防止恶意暴力尝试,防止洪水攻击、防止脚本自动提交等,验证码是一个较为便捷且行之有效的预防手段,这篇文章主要介绍了springboot图片验证码功能模块,需要的朋友可以参考下
    2022-04-04
  • java8 streamList转换使用详解

    java8 streamList转换使用详解

    这篇文章主要介绍了java8 streamList转换使用详解,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2020-08-08
  • Java基础之FileInputStream和FileOutputStream流详解

    Java基础之FileInputStream和FileOutputStream流详解

    这篇文章主要介绍了Java基础之FileInputStream和FileOutputStream流详解,文中有非常详细的代码示例,对正在学习java基础的小伙伴们有非常好的帮助,需要的朋友可以参考下
    2021-04-04

最新评论