深入解析Java ConcurrentHashMap类的源码

JDK8 ConcurrentHashMap computeIfAbsent bug分析 前段时间准备研究一波Mybatis,代码下载到IDEA,一路Debug走了一遍,头已经绕晕,准备放弃突然看到一些英文 上面写了啥bug 啥的,于是打开链接看了下,大概就是说的computeIfAbsent 方法,如果key存在的情况下也会加锁,会影响性能,后面又百度了下,发现还有其他bug 在写本文前,也看了些网上的文章,大概就是说的是在调用computeIfAbsent(key, …)方法时,正好其他线程需要在key对应位置插入结点,因为computeIfAbsent方法将位置设置了Reservati 阅读详情

前言

  ConcurrentHashMap是Java中的高并发容器,具有线程安全、高并发、高性能等特点。在本文中,我们将通过分析ConcurrentHashMap的数据结构、锁机制、LongAdder和computeIfAbsent方法的实现原理,来深入了解这一容器类。

ConcurrentHashMap的数据结构

  ConcurrentHashMap是基于哈希表实现的,其底层数据结构是由多个Segment组成的Segment数组,每个Segment内部都是一个与HashMap类似的链表结构(JDK1.7),或者是一个数组加链表结构(JDK1.8)。对于一个键值对,首先会根据其键值的哈希值定位到一个Segment,然后在该Segment内部进行操作。

  以下是ConcurrentHashMap的构造方法源码:

    public ConcurrentHashMap(int initialCapacity,
                             float loadFactor, int concurrencyLevel) {
        if (!(loadFactor > 0.0F) || initialCapacity < 0 || concurrencyLevel <= 0)
            throw new IllegalArgumentException();
        if (concurrencyLevel > MAX_SEGMENTS)
            concurrencyLevel = MAX_SEGMENTS;
        // java8之前
        int sshift = 0;
        int ssize = 1;
        while (ssize < concurrencyLevel) {
            ++sshift;
            ssize <<= 1;
        }
        this.segmentShift = 32 - sshift;
        this.segmentMask = ssize - 1;
        if (initialCapacity > MAXIMUM_CAPACITY)
            initialCapacity = MAXIMUM_CAPACITY;
        int c = initialCapacity / ssize;
        if (c * ssize < initialCapacity)
            ++c;
        int cap = MIN_SEGMENT_TABLE_CAPACITY;
        while (cap < c)
            cap <<= 1;
        // Segments数组中存放Segment对象
        Segment<K,V>[] segments = (Segment<K,V>[])new Segment[cap];
        // 构造每个Segment对象
        for (int i = 0; i < segments.length; ++i)
            segments[i] = new Segment<K,V>(loadFactor);
        this.segments = segments;
    }

  在源码中,我们可以看到ConcurrentHashMap的构造方法接收三个参数:initialCapacity表示初始容量大小,loadFactor表示负载因子,concurrencyLevel表示并发级别。其中,concurrencyLevel参数用来初始化ConcurrentHashMap的Segment数组大小,也就是ConcurrentHashMap内部开启的线程数量,因为每个Segment都会对应一个独立的线程。在初始化过程中,通过Segment数组进行ConcurrentHashMap的并发控制。

ConcurrentHashMap的锁机制

  在ConcurrentHashMap中,对于每个Segment,使用重入锁(ReentrantLock)来实现线程安全。对于已经锁定的Segment,在插入或删除元素时,只有等待其它线程操作完成后才能进行,以此避免多个线程同时操作同一个Segment导致的并发问题。而对于未被锁定的Segment,可以并发地执行读取操作,不会出现并发问题。

  以下是ConcurrentHashMap的Segment的构造方法和插入元素的方法源码:

    static class Segment<K,V> extends ReentrantLock implements Serializable {
        private transient volatile int count;
        transient int modCount;
        transient int threshold;
        transient volatile HashEntry<K,V>[] table;
        
        final float loadFactor;

        Segment(float lf) {
            this.loadFactor = lf;
            this.threshold = (int)(DEFAULT_CAPACITY * lf);
            this.table = new HashEntry[DEFAULT_CAPACITY];
        }
        
        V put(K key, int hash, V value, boolean onlyIfAbsent) {
          // 插入元素时先获取锁
          lock();
          // ...
        }
        
        // ...
    }

  在上述代码中,我们可以看到,在Segment的构造方法中通过继承ReentrantLock实现了对Segment的重入锁机制。而在插入元素时,则需要首先获取Segment的锁,保证只有一个线程在进行插入操作。

ConcurrentHashMap的并发度与性能

  ConcurrentHashMap的并发度和性能表现非常优秀,主要得益于其对读写操作的优化。由于ConcurrentHashMap在初始化时会指定内部开启的线程数量,因此可以避免像同步容器那样在进行读取、插入或删除操作时,多个线程被阻塞的问题,大大提升了并发性能。

  另外,ConcurrentHashMap中的元素插入和查询操作都使用了volatile关键字,保证对元素的修改和读取操作可见性,进一步提高了并发性能。

LongAdder的实现原理

  LongAdder是ConcurrentHashMap中的一个类,用于对值进行累加计数。与AtomicLong不同,LongAdder分散了计数,因此在高并发环境下比AtomicLong要快。具体来说,LongAdder使用了分段锁机制,将计数值分别记录在各自的cell中,每次更新时只需要对对应的cell进行加法计算,而不需要对所有的计数值进行加法计算。

  以下是LongAdder的源码:

    public class LongAdder {
        private static final Unsafe UNSAFE = Unsafe.getUnsafe();
        private static final long BASE;
        private static final int SHIFT;
        private static final int CELLSBUSY;
        private static final long[] PROBE;
        volatile transient Cell[] cells;
        public LongAdder() {
            this.cells = null;
        }

        public void add(long x) {
            Cell[] as;
            long b, v;
            int m;
            Cell a;
            if ((as = this.cells) != null || !this.casBase(b = this.base, b + x)) {
                boolean uncontended = true;
                if (as == null || (m = as.length - 1) < 0 || (a = as[getProbe() & m]) == null || !(uncontended = a.cas(v = a.value, v + x))) {
                    this.longAccumulate(x, (LongBinaryOperator)null, uncontended);
                }
            }

        }

        final void longAccumulate(long x, LongBinaryOperator fn, boolean wasUncontended) {
            int h;
            if ((h = getProbe()) == 0) {
                ThreadLocalRandom.current();
                h = getProbe();
                this.setProbe(h == 0 ? 1 : h);
                wasUncontended = true;
            }

            boolean collide = false;

            while(true) {
                Cell[] as;
                Cell a;
                int n;
                if ((as = this.cells) != null && (n = as.length) > 0) {
                    if ((a = as[h & n - 1]) == null) {
                        if (this.busy == 0) {
                            Cell r = new Cell(x);
                            if (this.busy == 0 && this.casBusy()) {
                                boolean created = false;

                                try {
                                    Cell[] rs;
                                    int m;
                                    int j;
                                    if ((rs = this.cells) != null && (m = rs.length) > 0 && rs[j = n - 1 & h] == null) {
                                        rs[j] = r;
                                        created = true;
                                    }
                                } finally {
                                    this.busy = 0;
                                }

                                if (created) {
                                    break;
                                }

                                continue;
                            }
                        }

                        collide = false;
                    } else if (!wasUncontended) {
                        wasUncontended = true;
                    } else {
                        long v = a.value;
                        if (a.cas(v, fn == null ? v + x : fn.applyAsLong(v, x))) {
                            break;
                        }

                        if (n >= CELLSBUSY || this.cells != as) {
                            collide = false;
                        } else if (!collide) {
                            collide = true;
                        } else if (this.busy == 0 && this.casBusy()) {
                            try {
                                if (this.cells == as) {
                                    Cell[] rs = new Cell[n << 1];

                                    for(int i = 0; i < n; ++i) {
                                        rs[i] = as[i];
                                    }

                                    this.cells = rs;
                                }
                            } finally {
                                this.busy = 0;
                            }

                            collide = false;
                            continue;
                        }
                    }

                    h ^= h << 13;
                    h ^= h >>> 17;
                    h ^= h << 5;
                    this.setProbe(h);
                    continue;
                }

                if (this.busy == 0 && this.cells == as && this.casBusy()) {
                    boolean init = false;

                    try {
                        if (this.cells == as) {
                            Cell[] rs = new Cell[2];
                            rs[h & 1] = new Cell(x);
                            this.cells = rs;
                            init = true;
                        }
                    } finally {
                        this.busy = 0;
                    }

                    if (init) {
                        break;
                    }
                    continue;
                }

                if (this.casBase(b, fn == null ? b + x : fn.applyAsLong(b, x))) {
                    break;
                }
            }

        }
        
        // ...
    }

  上述代码中,我们可以看到LongAdder实现的核心是通过Cell数组来记录各自的计数值,并使用分段锁机制实现并发控制,从而提高了并发性能。

computeIfAbsent方法的实现原理

  computeIfAbsent是ConcurrentHashMap中的一个方法,用于在ConcurrentHashMap中的键key不存在时,通过计算方式新增一个键值对。首先,computeIfAbsent方法会根据key的hash值得到其在ConcurrentHashMap中对应的Segment。然后,在该Segment内部进行操作:首先会对Segment进行加锁,然后判断该key是否存在,如果不存在,则通过Function计算出对应的value值,并插入到该Segment中。

  以下是computeIfAbsent方法的源码:

    public V computeIfAbsent(K key,
            Function<? super K, ? extends V> mappingFunction) {
        if (key == null || mappingFunction == null)
            throw new NullPointerException();
        // 计算key的哈希值
        int h = spread(key.hashCode());
        // 插入元素时先获取锁
        Segment<K,V> segment = segmentForHash(h);
        return segment.compute(key, h, mappingFunction, false);
    }

    public V compute(K key,
                    BiFunction<? super K, ? super V, ? extends V> remappingFunction) {
        if (key == null || remappingFunction == null)
            throw new NullPointerException();
        int h = spread(key.hashCode());
        Segment<K,V> segment = segmentForHash(h);
        return segment.compute(key, h,
                               (k, oldValue) -> {
                                   V newValue = remappingFunction.apply(k, oldValue);
                                   return (newValue == null) ?
                                          null :
                                          checkType(newValue);
                               },
                               true);
    }

  从上述代码中,我们可以看到,在Segment的compute方法中,如果key不存在于Segment中,则调用mappingFunction计算出对应的value,并插入到Segment中。compute方法使用了CAS无锁技术实现了线程安全。

总结

  ConcurrentHashMap是一个高并发容器,其源码实现比较复杂,本文从数据结构、锁机制、并发度和性能、LongAdder和computeIfAbsent等方面进行分析。无论是在学习Java并发编程,还是在实际应用中操作高并发容器,对于ConcurrentHashMap的深入了解都有重要的意义。

多线程高并发编程(10) -- ConcurrentHashMap源码分析   前文讲了HashMap的源码分析,从中可以看到下面的问题:  基于上述问题,都可以使用ConcurrentHashMap进行解决,ConcurrentHashMap使用分段锁技术解决了并发访问效率,在遍历迭代获取时进行修改操作也不会发生并发修改异常等等问题。 initTable:初始化 tabAt:寻找指定数组在内存中i位置的数据 helpTransfer:帮助扩容 transfer:扩容和数据迁移,采用多线程扩容,整个扩容过程,通过cas设置sizeCtl、tr 阅读详情

相关推荐

【多线程】ConcurrentHashMap1.8源码保姆级解析

基于一个while循环,先去判断数组初始化了没有,如果没有,会再次比较sizeCtl的情况,如果正在初始化,则会通过Thread.yield()让出CPU资源,如果没有初始化,则会通过CAS的方式修改sizeCtl的值为-1,修改过后还会再次判断数组是否被初始化了(DCL,以免在当前线程修改sizeCtl的值的时候,已经有别的线程对当前数组完成了初始化),如果当前数组仍然没有被初始化,才会真正开始初始化。>0:代表当前数组已经初始化完成,此时sizeCtl的值代表当前数组的扩容阈值,或者数组的初始化大小。

aqin1012的博客 1928

JAVA8 ConcurrentHashMap.computeIfAbsent 的使用及说明

1.简述 在JAVA8的Map接口中,增加了一个方法computeIfAbsent,此方法签名如下: public V computeIfAbsent(K key, Function<? super K,? extends V> mappingFunction) 如果mappingFunction(key)返回的值为null或抛出异常,则不会有记录存入map 此方法首先判断...

Luffy的专栏 1万+

手撕Java系列-ConcurrentHashMap详解1,概述,源码解析,节点类解析

`ConcurrentHashMap` 主要通过以下几个部分来实现线程安全和高性能: 1. **分段锁(Segment)**:将整个哈希表分成多个段,每个段是一个独立的锁。这样可以减少锁的竞争,提高并发性能。 2. **CAS 操作**:使用原子操作(Compare-and-Swap)来保证某些操作的线程安全。 3. **volatile 关键字**:使用 `volatile` 关键字来确保变量的可见性。 `ConcurrentHashMap` 通过分段锁(Segment)机制和 CAS 操作,实现了高

横竖撇折点 1397

性能有坑 | 慎用 Java 8 ConcurrentHashMap 的 computeIfAbsent

千万要注意 ConcurrentHashMap 的 computeIfAbsent 可能会有性能问题!

wu_weijie的博客 7342

深入解析JDK源码:方法注释与实现原理实战

深入理解JDK源码是掌握Java核心技术的必经之路。JDK不仅是Java程序运行的基础,其源码中蕴含了大量优秀的设计模式、高性能算法与线程安全机制。通过阅读OpenJDK项目中的核心类库实现,开发者能够洞察集合、并发、类加载等关键组件的底层运作原理。例如,ArrayList的动态扩容、的分段锁优化,皆源自对实际场景的深刻抽象。学习源码不仅能提升问题定位能力,更能指导高质量代码设计,为构建高并发、低延迟系统提供坚实支撑。在方法级别的JavaDoc中,@param@return和。

weixin_28729843的博客 774

liquibase扩展支持达梦数据库

近期项目中需要支持国产数据库,需要liquibase底层支持达梦数据库,记录下操作的详细步骤以便分享。 1.liquibase支持的数据库很多如MySQL, PostgreSQL, Oracle, Sql Server, DB2。但是达梦数据库并不支持,所以需要新建DMDatabase类来适配达梦数据库。达梦语法和Oracle很像所以只需要复制liquibase-core下的Oracle Database做些修改就可了。以下为需要修改的地方 public class DMDatabase extends A

qq_43677719的博客 5472

Java1.7ConcurrentHashMap类源码解析

先来看ConcurrentHashMap类的成员。 static final int DEFAULT_INITIAL_CAPACITY = 16; static final float DEFAULT_LOAD_FACTOR = 0.75f; static final int DEFAULT_CONCURRENCY_LEVEL = 16; static final int MAXI...

不说话装高手 568

Mac和VScode配置fortran

macOS系统 vscode fortran gfortran 终端

weixin_43047707的博客 2409

ConcurrentHashMap 1.7 源码分析

ConcurrentHashMap 1.7 原理 由于源码当中涉及Unsafe,先对其做一个解释 Unsafe介绍 1、Unsafe简介 Unsafe类相当于是一个java语言中的后门类,提供了硬件级别的原子操作,所以在一些并发编程中被大量使用。jdk已经作出说明,该类对程序员而言不是一个安全操作,在后续的jdk升级过程中,可能会禁用该类。所以这个类的使用是一把双刃剑,实际项目中谨慎使用,以免造成jdk升级不兼容问题。 2、Unsafe Api 这里并不系统讲解Unsafe的所有功能,只介绍和接下来内容

[爪洼ing]的博客 369

MacOS蓝牙连接疑难杂症:从基础排查到高级修复全攻略

本文提供了一份全面的MacOS蓝牙故障排查与修复指南。从基础的菜单栏检查、终端命令诊断,到软/硬重启蓝牙服务、删除配置文件,再到使用blueutil工具、重置NVRAM等高级方法,系统性地解决了蓝牙无法连接、连接不稳定等常见问题。最后还分享了环境干扰排查、预防措施及硬件故障自检方法,帮助用户彻底解决Mac蓝牙连接难题。

weixin_29271263的博客 449

新手必看!BUUCTF-PWN题实战:从零搭建Kali环境到成功RIP栈溢出

本文为CTF二进制安全新手提供了一份详实的BUUCTF-PWN入门指南。内容涵盖从零搭建Kali Linux环境、安装配置pwntools等核心工具,到深入浅出地讲解栈溢出原理与汇编基础,并手把手带领读者完成一道名为“rip”的经典栈溢出题目实战,成功利用漏洞获取系统权限。

t8u9v0w1x的博客 504

【Java 集合】ConcurrentHashMap (JDK 1.8 版本)

Map 一种存储键值对 (key-value) 的数据结构, 可以通过 key 快速地定位到需要的 value, 在 Java 中是一个使用频率很高的一个数据结构。一般情况下, 我们都是可以直接使用它的实现类 HashMap 就能满足需求了。但是 HashMap 在多线程情况, 并不是一个线程安全的类。使用在 Java 体系中古老的 Hashtable 作为替代, 该类基本上所有的方法都采用 synchronized 进行线程安全的控制, 虽然保证了线程安全, 但是牺牲了很大的性能。

LCN29的博客 2984

瑞芯微RK3506开发板Ethercat主站性能优化实战:从内核配置到微秒级延迟控制

本文详细介绍了在瑞芯微RK3506开发板上实现EtherCAT主站微秒级延迟控制的实战优化方案。核心在于部署Preempt-RT实时内核,并进行深度系统调优,包括CPU隔离、中断亲和性设置、内存锁定及电源管理调整。通过绕过Linux网络栈的专用驱动加载与配置,最终实现了稳定的高性能实时控制,满足工业自动化需求。

tcp8optimizer的博客 315

JUC---CAS源码解析(JDK13)

本篇介绍什么是CAS。 一、什么是CAS? 我认为某一个变量在内存的值是A,如果是的话,我就把它设置为B。如果不是的话,就不修改了。典型的乐观锁思想。避免多人同时修改出错。 CAS方法有3个参数,内存值V,预期值A,要修改为的值B。仅当V=A时,才将内存值修改为B。否则什么都不做。最后返回现在的V。 compareAndSet,compareAndSweap等等。比较并设置,比较并交换。等等操...

叭嘣的博客 477

指导初学者使用Anaconda运行GitHub上One - DM项目的步骤

指导初学者使用Anaconda运行GitHub上One - DM项目的步骤。

AI的内部世界的博客 1484

RISC-V嵌入式开发实战:手把手教你读懂并修改CH32V103的链接脚本(.ld)

本文深入解析RISC-V嵌入式开发中CH32V103链接脚本(.ld)的编写与优化技巧。从内存布局、段分配到实战调试,详细讲解如何解决程序跑飞、变量异常等典型问题,特别针对RISC-V架构的gp寄存器优化、中断向量表处理等特性提供专业指导,帮助开发者充分利用64KB Flash和20KB RAM资源。

weixin_33704234的博客 172

JUC---ThreadLocal源码解析(JDK13)

ThreadLocal -> 线程本地变量 强调同一个请求内(同一个线程内)不同方法间的共享 让某个需要用到的对象在线程间隔离(每个线程都有自己独有自己独立的对象) ThreadLocal的优点 达到线程安全 不需要加锁 提高执行效率 高效利用内存 节省开销 避免传参的麻烦 Thread、ThreadLocal、ThreadLocalMap三者的关系 每个Thread对象中都持有一...

叭嘣的博客 382

R语言数据可视化:绘制世界地图的两种方案

本文介绍了两种使用R语言绘制世界地图的方案:包含边界边框和不包含边界边框。无论您选择哪种方案,使用ggplot2包和相关的地理数据包,我们可以轻松地绘制出漂亮的世界地图。R语言提供了丰富的包和函数,使我们能够轻松地绘制世界地图,并根据需求添加边界边框。在本文中,我将介绍两种绘制世界地图的方案:一种包含边界边框,一种不包含边界边框。在上述代码中,我们使用了与方案一相同的数据准备和地图绘制步骤,但省略了添加边界边框的代码。函数的调用,我们可以绘制不包含边界边框的世界地图。方案二:绘制不包含边界边框的世界地图。

CodeMaven的博客 2225

Java并发包源码分析

并发是一种能并行运行多个程序或并行运行一个程序中多个部分的能力。如果程序中一个耗时的任务能以异步或并行的方式运行,那么整个程序的吞吐量和可交互性将大大改善。现代的PC都有多个CPU或一个CPU中有多个核,是否能合理运用多核的能力将成为一个大规模应用程序的关键。   Java基础部分知识总结点击Java并发基础总结。Java多线程相关类的实现都在Java的并发包concurrent,concurr...

追逐消失的记忆 195

pscad IEEE标准系统搭建说明

pscad IEEE标准系统搭建说明,包含典型的IEEE 9节点、14节点、30节点、39节点和118节点系统。内含标准测试系统的原始数据和潮流结果。

Camera2Demo

在Camera2 的基础上进行封装,使得Camera2一堆初始化逻辑与Activity分离,降低耦合度,完成预览拍照并保存的操作

JDK源码阅读顺序

很多java开发的小伙伴都会阅读jdk源码,然而确不知道应该从哪读起。以下为小编整理的通常所需阅读的源码范围。 标题为包名,后面序号为优先级1-4,优先级递减 1、java.lang 1) Object 1 2) String 1 3) AbstractStringBuilder 1 4) StringBuffer 1 5) StringBuilder 1 6) Boolean 2 7) Byt...

GarfieldEr007的专栏 759

数学建模模型总结

这是本人在两年内准备参加数学建模竞赛搜集到的模型资料,已经经过了本人的筛选和整理,希望能够对准备参加数学建模竞赛的学弟学妹们提供一些帮助!

问遍了身边的面试官朋友,我整理出这份 Java 集合高频面试题(2024年最新版)

导致死循环的根本原因是 JDK 1.7 扩容采用的是“头插法”,会导致同一索引位置的节点在扩容后顺序反掉,在并发插入触发扩容时形成环,从而产生死循环。而 JDK 1.8 之后采用的是“尾插法”,扩容后节点顺序不会反掉,不存在死循环问题。JDK 1.7.0 的扩容代码如下,用例子来看会好理解点。} while (e!= null);PS:这个流程较难理解,建议对着代码自己模拟走一遍。

2401_84584338的博客 1280

第一章 Java常用的并发类

注:本系列博客主要参考于《分布式Java应用:基础与实践》,林昊 著 1、常用的并发集合类 ConcurrentHashMap:线程安全的HashMap的实现 CopyOnWriteArrayList:线程安全且在读操作时无锁的ArrayList CopyOnWriteArr...

chuifuhuo6864的博客 79

7 ConcurrentHashMap 源码注释1

public class ConcurrentHashMap<K,V> extends AbstractMap<K,V> implements ConcurrentMap<K,V>, Serializable { private static final long serialVersionUID = 7249069246763182397L; /* ---------------- Constants -------------- */ /.

少伟的博客 296

集合类源码整理

集合是Java的核心知识,我们在日常的工作和学习中经常会使用到。集合作为一种容器,通常用到的就是它的增删改查功能。对我自己来说,虽然我会用,但是在相当长的一段时间里是知其然不知其所以然。学习集合的源码知识,了解他的底层数据结构是作为一名技术人员的必修课。在后续的一段时间,我会持续的更新相关的源码知识,如果大家感兴趣,请关注我,一起学习。每篇文章都会从集合的成员变量、涉及的数据结构、构造方法、常用方...

MAX_VALUE的博客 318

源码解析:CAS原子性如何保证?ABA问题如何解决?这下面试和实操你绝对会了!

源码解析:CAS原子性如何保证?ABA问题如何解决?这下面试和实操你绝对会了!

weixin_45673401的博客 336
上一篇: ArrayList源码分析——学习Java集合的必修课
下一篇: ThreadPoolExecutor类源码分析
JavaWizard-M
博客等级 码龄6年 3159粉丝 148原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值