ConcurrentHashMap源码解析

ConcurrentHashMap源码解析 JDK8 一、简介 上篇文章详细介绍了HashMap的源码及原理,本文趁热打铁继续分析ConcurrentHashMap的原理。 首先在看本文之前,希望对HashMap有一个详细的了解。不然看直接看ConcurrentHashMap的源码还是有些费劲的。 相信对HashMap,HashTable有一定了解,应该知道HashMap是不具备线程安全性的,在resize时会丢数据(JDK8),而HashTa... 阅读详情

作者: dreamcatcher-cx

出处: http://www.cnblogs.com/chengxiao/

1. 引言

ConcurrentHashMap 是 java 并发包中提供的一个线程安全且高效的 HashMap 实现,ConcurrentHashMap 在并发编程的场景中使用频率非常之高,本文就来分析下 ConcurrentHashMap 的实现原理,并对其实现原理进行分析(JDK1.7)。

2. 实现原理

众所周知,哈希表是非常高效,复杂度为 O(1) 的数据结构,在 Java 开发中,我们最常见到最频繁使用的就是 HashMap 和 HashTable,但是在线程竞争激烈的并发场景中使用都不够合理。

HashMap :先说 HashMap,HashMap 是 线程不安全,在并发环境下,可能会形成 环形链表 (扩容时可能造成),导致 get 操作时,cpu 空转,所以,在并发环境中使用 HashMap 是非常危险的。

HashTable:HashTable 和 HashMap 的实现原理几乎是一样,差别无非就是 1. HashTable 不允许 key 和 value 为 null;2. HashTable 是线程安全的。 但是 HashTable 线程安全的策略实现代价太大了,简单粗暴,get / put 所有相关操作都是 synchronized 的,这相当于给整个哈希表加了一把大锁,多线程访问的时候,只要有一个线程访问或操作该对象,那其他线程只能阻塞,相当于将所有的操作 串行化,在竞争激烈的并发场景中性能就会非常差。

HashTable 全标锁

HashTable 性能差主要是由于所有操作需要竞争同一把锁,而如果容器中有多把锁,每一把锁锁一段数据,这样在多线程访问时不同段的数据时,就不会存在锁竞争了,这样便可以有效地提高并发效率。这就是 ConcurrentHashMap 所采用的 分段锁 思想。

ConcurrentHashMap 分段锁

3. 源码分析

ConcurrentHashMap 采用了非常精妙的「分段锁」策略,ConcurrentHashMap 的主干是个 Segment 数组。

final Segment<K,V>[] segments;

Segment 继承了 ReentrantLock,所以它就是一种可重入锁(ReentrantLock)。在 ConcurrentHashMap,一个 Segment 就是一个子哈希表,Segment 里维护了一个 HashEntry 数组,并发环境下,对于不同 Segment 的数据进行操作是不用考虑锁竞争的。(就按默认的 ConcurrentLeve 为 16 来讲,理论上就允许 16 个线程并发执行,有木有很酷)

所以,对于同一个 Segment 的操作才需要考虑线程同步,不同的 Segment 则无需考虑。

Segment 类似于 HashMap,一个 Segment 维护着一个 HashEntry 数组。

transient volatile HashEntry<k,v>[] table;

HashEntry 是目前我们提到的最小的逻辑处理单元了。一个 ConcurrentHashMap 维护一个 Segment 数组,一个 Segment 维护一个 HashEntry 数组。

static final class HashEntry<K,V>{
    final int hash;
    final K key;
    volatile V value;
    volatile HashEntry<K,V> next;
    // 。。。
}

我们说 Segment 类似哈希表,那么一些属性就跟我们之前提到的 HashMap 差不了多少,比如负载因子 loadFactor,比如阈值 threshold 等等,看下 Segment 的构造方法:

Segment(float lf,int threshold,HashEntry<K,V> tab){
    this.loadFactor = lf;         // 负载因子
    this.threshold = threshold;   // 阈值
    this.table = tab;             // 主干数组即 HashEntry 数组 
}

我们来看下 ConcurrentHashMap 的构造方法

public ConcurrentHashMap(int initialCapacity,float loadFactor,int concurrencyLevel){
    if(!(loadFactor)>0 || initialCapacity<0 || concurrencyLevel <= 0)
        throw new IllegalArgumentException();
    // MAX_SEGEMENTS 为 1<<16=65536 也就是最大并发数位 65536
    if(concurrencyLevel > MAX_SEGMENTS)
        concurrencyLevel = MAX_SEGMENTS;
    // 2 的 sshif 次方等于 ssize,例:ssize=16,sshift=4;ssize=32,sshif=5
    int sshift = 0;
    // ssize 为 segments 数组的长度,根据 concurrentLevel 计算得出
    while(ssize<concurrencyLevel){
        ++sshift;
        ssize <<= 1;
    }
    // segmentShift 和 segmentMask 这两个变量在定位 segment 时会用到,后面会详细讲
    this.segmentShift = 32 - sshift;
    this.segmentMask = ssize -1;
    if(initialCapacity>MAXIMUM_CAPACITY)
        initialCapacity = MAXIMUM_CAPACITY;
    // 计算 cap 的大小,即 Segment 中 hashEntry 的数组长度,cap 也一定为 2 的 n 次方
    int c = initialCapacity / ssize;
    if(c*ssize<initialCapacity)
        ++c;
    int cap = MIN_SEGMENT_TABLE_CAPACITY;
    while(cap<c)
        cap<<1;
    // 创建 segments 数组并初始化第一个 Segment,其余的 Segment 延迟初始化
    Segment<K,V> s0 = new Segment<K,V>(loadFactor,(int)(cap*loadFactor),(HashEntry<K,V>[])new HashEntry[cap]);
    Segment<K,V>[] ss = (Segment<K,V>[]) new Segment[ssize];
    UNSAFE.putOrderedObject(ss,SBASE,s0);
    this.segments = ss;
}

初始化方法有三个参数,如果用户不指定则会使用默认值,initialCapacity 为 16,loadFactor 为 0.75(负载因子,扩容时需要参考),concurrentLevel 为 16。

从上面的代码可以看出来,Segment 数组的大小 ssize 是由 concurrentLevel 来决定的,但是却不一定是 concurrentLevel,ssize 一定是大于或等于 concurrentLevel 的最小 2 的次幂。比如:默认情况下 concurrentLevel 是 16,则 ssize 为 16;弱 concurrentLevel 为 14,ssize 为 16;若 concurrentLevel 为 17,则 ssize 为 32。为什么 Segment 的数组大小一定是 2 的次幂?其实主要是便于通过按位与的散列算法来定位 Segment 的 index。至于更详细的原因,有兴趣的话可以参考另一篇 文章 ,其中对于数组长度为什么一定要是 2 的次幂有较为详细的分析。

接下来,我们看一下 put 方法

public V put(K key,V value){
    Segment<K,V> s;
    // concurrentHashMap 不允许 key/value 为空
    if(value == null)
        throw new NullPointerException();
    // hash 函数对 key 的 hashCode 重新散列,避免差劲的不合理的 hashcode,保证三类均匀。
    int hash = hash(key);
    // 返回的 hash 值无符号右移 segementShift 位与端掩码进行位运算,定位 segment
    int j = (hash >>> segmentShift) & segmentMashk;
    if((s=(Segment<K,V>)UNSAFE.getObject(segments,(j<<SSHIFT)+SBASE))==null)
        s = ensureSegment(j);
    return s.put(key,hash,value,false);     
}

从源码看出,put 的方法主要逻辑也就两步:1. 定位 segment 并确保定位 Segment 已经初始化;2.调用 Segment 的 put 方法

4. segmentShift 和 segmentMask

segmentShift 和 segmentMask 这两个全局变量的主要作用是用来定位 Segment,int j = (hash >>> segmentShift) & segmentMask

segmentMask:段掩码,假如 segments 数组长度为 16,则段掩码为 16-1=15;segments 长度为 32,段掩码为 32-1=31。这样等到的所有 bit 为都为 1,可以更好地保证散列的均匀性

segmentShift:2 的 sshift 次方等于 ssize,segmentShift = 32-sshift。若 segments 长度为 16,segmentShift = 32-4 = 28;若 segments 长度为 32,segmentShift = 32-5 = 27.而计算得出的 hash 值最大为 32 位,无符号右移 segmentShift,则意味着只保留高几位(其余位是没用的),然后与段掩码 segmentMask 为运算来定位 Segment。

get 方法:

public V get(Object key){
    Segment<K,V> s;
    HashEntry<K,V>[] tab;
    int h = hash(key);
    long u = (((h>>>segmentShift) & segmentMask)<<sshift) + SBASE;
    // 先定位到 Segment,在定位到 HashEntry
    if((s = (Segment<K,V>)UNSAFE.getObjectVolatile(segments, u)) != null && (tab=s.table)!=null){
        for(HashEntry<K,V> e = (HashEntry<K,V>) UNSAFE.getObjectVolatile(tab, ((long)(((tab.length - 1) & h)) << TSHIFT) + TBASE);e!=null;e=e.next){
            K k;
            if((k=e.key)==key || (e.hash==h && key.equals(k))){
                return e.value;
            }
        }
    }
    return null;
}

get 方法无需加锁,由于其中涉及到的共享变量都是用 volatile 修饰,volatile 可以保证内存可见性。只不过是锁粒度细了而已 。

put 方法:

ConcurrentHashMap put 方法是调用了 Segment 的 put 方法,Segment 的 put 方法是要加锁的,只不过是锁粒度细了而已。

final V put(K key, int hash, V value, boolean onlyIfAbsent) {
            HashEntry<K,V> node = tryLock() ? null :
                scanAndLockForPut(key, hash, value);//tryLock不成功时会遍历定位到的HashEnry位置的链表(遍历主要是为了使CPU缓存链表),若找不到,则创建HashEntry。tryLock一定次数后(MAX_SCAN_RETRIES变量决定),则lock。若遍历过程中,由于其他线程的操作导致链表头结点变化,则需要重新遍历。
            V oldValue;
            try {
                HashEntry<K,V>[] tab = table;
                int index = (tab.length - 1) & hash;//定位HashEntry,可以看到,这个hash值在定位Segment时和在Segment中定位HashEntry都会用到,只不过定位Segment时只用到高几位。
                HashEntry<K,V> first = entryAt(tab, index);
                for (HashEntry<K,V> e = first;;) {
                    if (e != null) {
                        K k;
                        if ((k = e.key) == key ||
                            (e.hash == hash && key.equals(k))) {
                            oldValue = e.value;
                            if (!onlyIfAbsent) {
                                e.value = value;
                                ++modCount;
                            }
                            break;
                        }
                        e = e.next;
                    }
                    else {
                        if (node != null)
                            node.setNext(first);
                        else
                            node = new HashEntry<K,V>(hash, key, value, first);
                        int c = count + 1;
              //若c超出阈值threshold,需要扩容并rehash。扩容后的容量是当前容量的2倍。这样可以最大程度避免之前散列好的entry重新散列,具体在另一篇文章中有详细分析,不赘述。扩容并rehash的这个过程是比较消耗资源的。
                        if (c > threshold && tab.length < MAXIMUM_CAPACITY)
                            rehash(node);
                        else
                            setEntryAt(tab, index, node);
                        ++modCount;
                        count = c;
                        oldValue = null;
                        break;
                    }
                }
            } finally {
                unlock();
            }
            return oldValue;
        }

5. 总结

ConcurrentHashMap 作为一种线程安全且高效的哈希表解决方案,尤其其中的 “分段锁” 的方案,相比 HashTable 的全表锁在性能上的提升非常之大。

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

相关推荐

手把手教你解决uniapp中esbuild版本冲突问题(Mac版专属指南)

本文是针对Mac平台uniapp开发者的专属指南,详细解析了运行项目时常见的`Cannot start service`错误,即esbuild的Host版本与binary版本不匹配问题。文章从错误根源入手,提供了从快速应急处理到彻底根治的完整解决方案,包括手动替换二进制文件、锁定依赖版本、清理缓存及优化工具链配置,旨在帮助开发者高效解决版本冲突,恢复并稳定开发环境。

i0j1k2l3m的博客 459

ConcurrentHashMap 源码解析

ConcurrentHashMap 是 HashMap 的线程安全版本,其内部和 HashMap 一样,都是采用了数组 + 链表 + 红黑树的方式来实现。如何实现线程的安全性?加锁。但是这个锁应该怎么加呢?

流华追梦的专栏 1276

YonSuite如何新增用户并分配角色角色授权图文教程

用友YonSuite产品基础教学

ct341688的博客 620

JDK8 ConcurrentHashMap源码解析

4、把元素放入槽内。在第一次扩容的时候,sizeCtl会被设置为一个很大负数,之后每一个线程扩容的时候sizeCtl+1,每一个线程扩容完成之后,sizeCtl减1。2、加锁的粒度,并非整个ConcurrentHashMap,而是对每个头节点加锁,即并发度就是Node数组的长度,初始长度为16,和在JDK7中的Segment的个数相同。3、在线程扩容未完成之前,有的数组下标已经迁移完毕,有的还在旧的HashMap中,当使用get的时候还是会访问旧的HashMap,怎么处理呢?3、提供扩容的并发度。

cggghh的博客 1113

Cadence笔记--原理图导入PCB

根据PCB尺寸画PCB外观尺寸,这里注意X\Y不能超过指定的区域,X\Y的最大值在setuo的grids里设置,画完后保存并关闭工程然后重新打开(不理解为何)由于只画了一个PMU6050,所以这里只有一个器件。工程选择board,选择存储路径和名称。6、重新网格导入,保证网表导入无误;注意网表保存的路径。

weixin_51418325的博客 1963

AD/Protel老手才知道的PCB过孔查看技巧:3分钟分清通孔、盲孔和埋孔

本文为PCB设计工程师提供了在Altium Designer/Protel中快速鉴别通孔、盲孔和埋孔的实用技巧。通过层叠管理器、高级筛选工具和实战五步法,帮助用户在3分钟内精准识别过孔类型,提升设计效率和电路性能。特别适合处理高密度HDI板卡中的复杂过孔布局。

weixin_29164497的博客 576

java集合—— ConcurrentHashmap源码解析

基于JDK 8解析常用集合——ConcurrentHashmap,从源码入手分操作一步一步了解原理,简单易懂,怎样保证线程安全的? 通过了哪些手段保证了性能?扩容同时删除或者查询有影响吗?

共勉 895

【车辆动力学】专栏目录汇总

车辆动力学知识目录汇总

CrazyRabbit的博客 1714

java集合篇之ConcurrentHashMap解析与源码解读

CAS 是一种原子操作,其原子性由 CPU 的硬件指令(如CMPXCHG)提供支持。CAS 的不可分割性和内存屏障确保了操作的完整性和一致性,使其成为实现无锁编程和高效并发的重要工具。然而,CAS 也有一些局限性,需要在实际应用中结合其他技术来解决。

庄子兄弟的博客 1148

期末求生指南:手把手教你用F12开发者工具绕过百一测评的切屏检测(JavaScript/jQuery实战)

本文详细介绍了如何利用F12开发者工具绕过百一测评的切屏检测,通过分析JavaScript/jQuery中的blur()和focus()事件监听机制,提供实战技巧动态修改代码。文章涵盖事件监听定位、代码修改方法及应对高级检测机制的策略,适合需要临时解决线上考试限制的学生参考。

weixin_30731305的博客 448

华为od统一考试B卷【单词搜索】C++ 实现

找到它是一个小游戏,你需要在一个矩阵中找到给定的单词。假设给定单词 HELLOWORD,在矩阵中只要能找到 H->E->L->L->O->W->O->R->L->D连成的单词,就算通过。意区分英文字母大小写,并且您只能上下左右行走,不能走回头路。

MISAYAONE的博客 712

Java之ConcurrentHashMap源码解析

ConcurrentHashMap源码解析 文章目录ConcurrentHashMap源码解析jdk8之前的实现原理jdk8的实现原理变量解释初始化初始化tableput操作hash算法获取table中对应的元素f链表或红黑树操作table 扩容get操作 jdk8之前的实现原理 jdk8的实现原理 JDK8的实现已经抛弃了Segment分段锁机制,利用CAS+Synchronized来保证并发更...

https://gitee.com/micai-code 501

CAP CDS 和 ABAP Cloud 的 CDS 有什么区别?

摘要:SAP生态中的CAP CDS与ABAP CDS虽同名但存在显著差异。CAP CDS定位为全栈应用的领域模型与服务契约语言,支持声明式建模并直接编译为服务接口;而ABAP CDS更侧重数据语义层,与ABAP平台深度集成,服务于SQL优化和服务发布链路。两者在语法、持久化方式、服务暴露机制及权限模型上均有不同:CAP CDS采用CDL语法,支持模型即服务;ABAP CDS基于ABAP DDL,通过视图定义语义并与RAP框架紧密耦合。这些差异反映了二者在不同平台中的设计哲学和应用场景。

2007 年 ~ 2025 年,深耕 SAP 技术 18 年 205

并发编程笔记七-ConcurrentHashMap源码深度解析上

红黑树是一颗特殊的平衡二叉树,我们对数据结构有一点了解的应该清楚,平衡二叉树有一个特性是左右子树的高度差不能大于1,如果超出1,就会进行左旋和右旋的操作实现自平衡。而红黑树虽然没有左右子树高度差不能大于1的限制,但其也保留了左旋和右旋的操作,并且在这个基础上还会有变色的特点,这里我先来讲一下红黑树的五个平衡条件:1. 所有节点都是黑色或者红色;2. 根节点必须为黑色;

qq_41191331的博客 721

华为的C语言编程规范.pdf

华为技术有限公司内部技术规范。本规范制定了编写C语言程序的基本原则、规则和建议。从代码的清晰、简洁、可测试、安全、程序效率、可移植各个方面对C语言编程作出了具体指导。

C# WinForm 基于DirectShow.dll编写的程序,可边预览边保存视频,音频和图片

基于微软的DirectShow.dll的二次开发1.自动识别连接的摄像头跟麦克风且可以对其进行选择(默认选中第一个)2.可录制wmv和avi两种格式的视频并且都带有音频,avi可以录制的分辨率和帧数进行选择,wmv默认进行了淸晰度优化如不喜欢可去除代码中有相关提示3.可在预览点开之后进行拍照,也可边录制视频边拍照4.添加了视频和音频的压缩功能(酌情使用)这次二次开发发现C#对DirectShow的相关信息太少,这次做完也不敢藏私共享出来,还望大家指正

ConcurrentHashMap实现原理及源码解析

ConcurrentHashMap实现原理及源码解析 1.为什么要用ConcurrentHashMap? HashMap线程不安全,而Hashtable是线程安全,但是它使用了synchronized进行方法同步,插入、读取数据都使用了synchronized,当插入数据的时候不能进行读取(相当于把整个Hashtable都锁住了,全表锁),当多线程并发的情况下都要竞争同一把锁,导致效率极其低下.而...

mry6的博客 982

霍尼韦尔SoftMaster使用说明

霍尼韦尔SoftMaster使用说明

JDK1.8源码解析之ConcurrentHashMap

ConcurrentHashMap源码解析概述目录结构put方法putAlladdCounthelpTransfertryPresizetransfersize如何插入一段漂亮的代码片生成一个适合你的列表创建一个表格设定内容居中、居左、居右SmartyPants创建一个自定义列表如何创建一个注脚注释也是必不可少的KaTeX数学公式新的甘特图功能,丰富你的文章UML 图表FLowchart流程图导出...

m0_37148920的博客 630

ConcurrentHashMap图文源码解析

同HashMap一样,Segment包含一个HashEntry数组,数组中的每一个HashEntry既是一个键值对,也是一个链表的头节点。Size方法的目的是统计ConcurrentHashMap的总元素数量, 自然需要把各个Segment内部的元素数量汇总起来。数组中的每一个Entry元素,又是一个链表的头节点。但是,如果在统计Segment元素数量的过程中,已统计过的Segment瞬间插入新的元素,这时候该怎么办呢?Segment的写入是需要上锁的,因此对同一Segment的并发写入会被阻塞。

echola_mendes的博客 1349

ConcurrentHashMap源码逐行解析

简介: 因为博主前面写过HashMap的解析,所以这里只分析和HashMap不同点,类似的地方会滤过,有需要的朋友可以结合HashMap的文章一起看。 ConcurrentHashMap是HashMap的升级,是线程安全的。

couguolede的博客 1731
上一篇: Hashtable 和 HashMap 的区别
下一篇: Integer.valueOf 源码分析
firepation
博客等级 码龄10年 2254粉丝 265原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

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

抵扣说明:

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

余额充值