【ConcurrentHashMap】JDK1.7版本源码解读与分析

news2024/9/23 11:20:32

如果对文章中提到的与 HashMap 相关的部分有任何疑问, 请移步HashMap源码详解

简介

在这里插入图片描述

  • 底层是一个 Segment[] 数组, 每个 Segment对象 内部又有一个 Entry[ ] 数组, 一个 Entry[] 数组就相当于一个HashMap

  • Entry[ ]采用拉链法解决冲突, 但是没有红黑树, 红黑树是1.8才引入的;

  • 一个 Segment对象就是一个段; 当往容器中添加元素调用 put 方法时, 锁的粒度就是一个段;

  • 调用 put 方法时, 先计算应该放到 Segment[ ] 中的哪个段, 然后调用 segment.put() 方法将 Entry 插入到 segment.Entry[ ],可以看出, 一次插入有两次散列,一次负责选择在 Segment[] 中的位置, 一次负责选择 Segment 内 Entry[] 的位置;

  • 两次散列使用的 hash 值不同;

  • Segment数组的长度是固定的, 构造以后就不会再扩容, 内部的Entry[ ] 是一个个独立的Map, 会各自扩容, 互相之间的大小没有必然关系;

  • ConcurrentHashMap拉链时采用的是头插法;

构造方法

  • 默认的 initialCapacity = 16, loadFactor = 0.75, concurrentcyLevel = 16;

  • concurrencyLevel 经一些计算, 得到 Segment数组 的长度, 并且不再改变.

    具体逻辑为: 取 >= concurrencyLevel 的, 最小的, 2的整数次幂作为 Segment 数组的长度; 原理是将 1 不断左移, 直到 >= concurrencyLevel参数;

  • initialCapacity 和 concurrencyLevel 经计算得到 segment[0] 中Entry数组的长度;

    具体逻辑为: (initialCapacity / concurrentcyLevel) 向上取整得tmp, 如果tmp <= 2, 直接取 capacity = 2; 如果tmp > 2, 再取 >= temp 的, 最小的, 2的整数次幂;

  • 在默认情况下, 最终 Segment 数组长度为 16, 一个 Segment 内部的 Entry 数组长度为 2;

  • 根据计算出的尺寸, 创建一个 Segment 对象 s0;

  • 使用 UNSAFE.putOrderedObject(ss, SBASE, s0) 系统调用将 s0 直接放到 segment数组 下标为0的位置;

  • 经过构造后, 只有segment[0] 的位置有 非null segment对象, 其余位置将在各自首次添加元素的时候复制 segment[0] 的参数, 进行初始化; s0 就是一个原型;

public ConcurrentHashMap(int initialCapacity,
                             float loadFactor, int concurrencyLevel) {
        if (!(loadFactor > 0) || initialCapacity < 0 || concurrencyLevel <= 0)
            throw new IllegalArgumentException();
        if (concurrencyLevel > MAX_SEGMENTS)
            concurrencyLevel = MAX_SEGMENTS;
        // Find power-of-two sizes best matching arguments
        int sshift = 0;
        int ssize = 1; // 用于记录segment数组的长度
        while (ssize < concurrencyLevel) {
            ++sshift;
            // 通过不断左移的方式得到 >= concurrencyLevel 的2的整数次幂, 时间复杂度为32
            ssize <<= 1;
        }
        this.segmentShift = 32 - sshift; // 散列到segements和enntries时要使用不同的hash值, 通过hash值移位实现
    	// 用于取&, 代替取模操作, 因为segment数组长度不会再变化, 所以这里记录下来, 以后直接用
        this.segmentMask = ssize - 1; 
        if (initialCapacity > MAXIMUM_CAPACITY)
            initialCapacity = MAXIMUM_CAPACITY;
        int c = initialCapacity / ssize; // 用于记录Entry数组长度
        if (c * ssize < initialCapacity)
            ++c; // 相当于向上取整 
        int cap = MIN_SEGMENT_TABLE_CAPACITY; // 保证最小为2
        while (cap < c)
            cap <<= 1; // 取2的整数次幂
    
    	// new 出一个segment对象, 作为 ss[0]
        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];
    	// 使用系统调用将 s0 放到 ss[0]
        UNSAFE.putOrderedObject(ss, SBASE, s0); // ordered write of segments[0]
        this.segments = ss;
    }

hash方法

  • 通过一个 seed 和 key.hashCode() 异或, 再进行移位相加, 异或 等操作得到Hash值
private int hash(Object k) {
        int h = hashSeed;

        if ((0 != h) && (k instanceof String)) {
            return sun.misc.Hashing.stringHash32((String) k);
        }

        h ^= k.hashCode();

        // Spread bits to regularize both segment and index locations,
        // using variant of single-word Wang/Jenkins hash.
        h += (h <<  15) ^ 0xffffcd7d;
        h ^= (h >>> 10);
        h += (h <<   3);
        h ^= (h >>>  6);
        h += (h <<   2) + (h << 14);
        return h ^ (h >>> 16);
}

put方法

  • 不允许以 null 作为 key;
  • ConcurrentHashMap不允许以null作为value, 这是为了防止歧义; 在HashMap中, 串行执行的情况下, 如果 get(key0) == null, 可以使用containsKey方法确定是否存在key0; 而在多线程环境下, 如果使用相同的方式, 即使后面调用containsKey发现存在key0, 那你也不能确定key0是不是在你get之后由别的线程插入的; 也就是说无法确定在你调用get方法时得到 null 的瞬间, 究竟是不存在key0? 还是存在key0但是value == null
  • JDK1.8中, ConcurrentHashMap判断两个键值对是否重复的逻辑是 key 的hash值 == 且 (key == 或 equals), 而ConcurrentHashMap的判断逻辑是 key== 或 ( hash== 且 key.equals )
 public V put(K key, V value) {
	Segment<K,V> s;
	if (value == null)
	    throw new NullPointerException();
	int hash = hash(key);
	// 通过 hash >>> segmentShift取一个新的hash值进行散列, 确定要放在哪个段;
	int j = (hash >>> segmentShift) & segmentMask; 
	// 如果要放入的位置 segment 引用为 null 的话, 调用ensureSegment生成 segment 对象放入对应位置
	if ((s = (Segment<K,V>)UNSAFE.getObject        
	     (segments, (j << SSHIFT) + SBASE)) == null) 
	    s = ensureSegment(j);
	// 拿到segment对象, 调用其 put 方法, 将键值对放到其内部的entry中
	// 直接用原hash值, 这样两次散列使用的hash值就不一样
	return s.put(key, hash, value, false); 
}
  • ensureSegment方法中, 通过不断判断 ss[k] 上是否已经有 非null 引用来保证线程安全;
  • 最终结果就是以当前 segments[0] 中保存的大小参数, 来 new 一个 segment 对象, 放入 ss 对应位置, 并将该对象返回
private Segment<K,V> ensureSegment(int k) {
        final Segment<K,V>[] ss = this.segments;
        long u = (k << SSHIFT) + SBASE; // raw offset
        Segment<K,V> seg;
        if ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u)) == null) {
            Segment<K,V> proto = ss[0]; // use segment 0 as prototype
            int cap = proto.table.length;
            float lf = proto.loadFactor;
            int threshold = (int)(cap * lf);
            HashEntry<K,V>[] tab = (HashEntry<K,V>[])new HashEntry[cap];
            if ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u))
                == null) { // recheck
                Segment<K,V> s = new Segment<K,V>(lf, threshold, tab);
                while ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u))
                       == null) {
                    if (UNSAFE.compareAndSwapObject(ss, u, null, seg = s))
                        break;
                }
            }
        }
        return seg;
    }
  • Segment::put方法中, 使用锁机制保证线程安全;
  • static final class Segment<K,V> extends ReentrantLock implements Serializable; Segment类继承了ReentrantLock, 也就是说segment对象本身就可以充当一个锁
  • segment内部的put方法, 加锁成功后的逻辑和 HashMap 基本上是一样的, 重点看scanAndLockForPut方法
final V put(K key, int hash, V value, boolean onlyIfAbsent) {
    // 在这里尝试上锁, 如果加锁失败, 调用scanAndLockForPut, scanAndLockForPut成功加锁后才会返回;
	HashEntry<K,V> node = tryLock() ? null : scanAndLockForPut(key, hash, value);
	V oldValue;
	try {
	    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;
	            if ((k = e.key) == key ||
	                (e.hash == hash && key.equals(k))) {
	                // ... 和HashMap逻辑一样
                    // rehash扩容方法也被包裹在内
	} finally {
	    unlock();
	}
	return oldValue;
}
  • 在内部尝试提前创建Node, 创建完成后
private HashEntry<K,V> scanAndLockForPut(K key, int hash, V value) {
	HashEntry<K,V> first = entryForHash(this, hash); // 要散列到的位置的第一个Entry
	HashEntry<K,V> e = first;
	HashEntry<K,V> node = null;
	int retries = -1; // negative while locating node
	while (!tryLock()) {
	    HashEntry<K,V> f; 
        // 1.尝试创建Node的分支
	    if (retries < 0) { 
	        if (e == null) {
	            if (node == null) // 已经找到要插入的位置, 提前构建Node
	                node = new HashEntry<K,V>(hash, key, value, null);
	            retries = 0; // 已经创建Node, 不再进入此分支
	        }
            // 有重复结点, 直接替换value即可, retries = 0, 不再进入尝试创建Node的分支
	        else if (key.equals(e.key)) 
	            retries = 0;
	        else
	            e = e.next;
	    }
        // 3.循环MAX_SCAN_RETRIES之后, 不再while占用CPU资源了, 直接阻塞Lock
	    else if (++retries > MAX_SCAN_RETRIES) { // MAX_SCAN_RETRIES = 64
	        lock();
	        break;
	    }
        // 2.不再进入尝试创建Node的分支后, 在这里先循环 MAX_SCAN_RETRIES/2 次
        // 在奇数次进入的时候, 判断当前的链表头结点是否已经改变, 如果改变, 说明有其它线程put了Entry
        // 那么就要令 retries 再= -1 , 重新进入尝试创建的分支, 再检查是否有key重复的结点, 因为新添加的Entry可能重复
	    else if ((retries & 1) == 0 &&
	             (f = entryForHash(this, hash)) != first) {
	        e = first = f; // re-traverse if entry changed
	        retries = -1;
	    }
	}
	return node;
}

扩容机制

  • 扩容函数名字是 rehash() ;
  • 扩容操作是被包裹在 segment.put 方法内部的锁的作用范围之内的; 所以必然是线程安全的
  • 其余的扩容机制和 HashMap 一致

重点 和1.8的区别

  1. JDK1.8的时候, 不再采用分段的模式, 而是在 HashMap 的基础上, 采用 CAS 操作和 synchronized 实现线程安全;

  2. JDK1.8 的 ConcurrentHashMap 大体上和 HashMap 是一样的; 区别是:

    1. 插入的时候, 如果对应位置为null, 使用 CAS 的方式插入; 不是 null 则 synchronized 内完成插入
    2. 删除的时候, synchronized 内删除;
    3. 扩容的时候, CAS 的方式, 多线程扩容;
  3. CHM 为什么换成 JDK1.8 的实现?

    1. 代码和 HashMap 基本一致, 更清晰简单;
    2. 采用 CAS 和 synchronized 一起实现线程安全, 插入操作锁的是 Entry 数组的一个下标, 并发度更高了; 并且实现了安全的多线程扩容;

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1996731.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

音频进阶学习一——模拟信号和数字信号

文章目录 前言|版本声明&#xff1a;山河君&#xff0c;未经博主允许&#xff0c;禁止转载 一、什么是模拟信号和数字信号信号模拟信号数字信号数字和模拟信号的区别一览 二、信号处理系统总结 前言 所有软件的运行都得益于硬件上的突破&#xff0c;数字信号是从40年前就开始高…

达梦数据库 物理备份还原

达梦的物理备份还原 1.背景2.要求3.实验步骤3.1 相关术语3.2 准备工作3.3 联机备份还原3.3.1 数据备份3.3.1.1 手动备份3.3.1.2 定时备份 3.3.2 管理备份3.3.2.1 备份目录管理3.3.2.2 备份集校验与删除 3.3.3 数据还原 3.4 脱机备份还原3.4.1 DMRMAN工具3.4.2 数据备份3.4.2.1 …

https://ffmpeg.org/

https://ffmpeg.org/ https://www.gyan.dev/ffmpeg/builds/ https://github.com/BtbN/FFmpeg-Builds/releases F:\Document_ffmpeg F:\Document_ffmpeg\ffmpeg-master-latest-win64-gpl-shared\bin

python模式设计代码之观察者模式

1、观察者模式 话题订阅模式。观察者模式有两个角色&#xff0c;分别是话题发布者和话题订阅者&#xff08;即观察者&#xff09;。发布者就是把消息发送给话题&#xff0c;观察者就是订阅这个话题从而得到最新的资讯。这个模式的作用就拿手机的消息推送来说&#xff0c;app身…

深入C# .NET核心:委托与事件机制全解析

摘要&#xff1a; 在C# .NET编程中&#xff0c;委托和事件是实现异步编程和对象间通信的关键机制。理解它们的工作原理对于编写高效、响应式的应用程序至关重要。本文将深入探讨C# .NET中的委托与事件&#xff0c;从基础概念到高级应用&#xff0c;为读者提供全面的指导。 正文…

如何提高游戏的可玩性和趣味性?

提高游戏的可玩性和趣味性是吸引玩家并保持他们长期参与的关键。以下是一些策略和建议&#xff0c;可以帮助您增强游戏的吸引力和娱乐价值&#xff1a; 1. 独特的游戏机制 创新玩法&#xff1a;开发新颖、独特的游戏机制&#xff0c;让玩家在体验中感受到前所未有的乐趣。避免…

【网络编程】字节序,IP地址、点分十进制、TCP与UDP的异同

记录学习&#xff0c;思维导图绘制 目录 1、字节序​编辑 2、IP地址 3、点分十进制 4、TCP与UDP的异同 1、字节序 2、IP地址 3、点分十进制 4、TCP与UDP的异同

STL源码刨析:红黑树(RB-tree)

目录 1.前言 2.RB-tree的简单介绍 3.RB-tree的插入节点操作 4.RB-tree的删除节点操作 5.RB-tree的节点设计 6.RB-tree的迭代器设计 7.RB-tree的数据结构 8.RB-tree的构造与内存管理 9.RB-treed的元素操作 前言 在文章《STL源码刨析&#xff1a;树的导览》中&#xff0c;曾简单的…

使用 MongoDB 构建 AI:Flagler Health 的 AI 旅程如何彻底改变患者护理

Flagler Health 致力于为慢性病患者提供支持&#xff0c;为其匹配合适的医生以提供合适的护理。 通常&#xff0c;身患严重病痛的患者面临的选择有限&#xff0c;他们往往需要长期服用阿片类药物&#xff0c;或寻求成本高昂的侵入性外科手术干预。遗憾的是&#xff0c;后一种方…

linux小组件:git

git是什么&#xff1f; git是版本控制器&#xff08;去中心化的分布式系统&#xff09;可以快速高效地处理从小型到大型的各种项目。易于学习&#xff0c;占地面积小&#xff0c;性能极快。它具有廉价的本地库&#xff0c;方便的暂存区域和多个工作流分支等特性。 什么叫版本…

【数据结构七夕专属版】单链表及单链表的实现【附源码和源码讲解】

本篇是博主在学习数据结构时的心得&#xff0c;希望能够帮助到大家&#xff0c;也许有些许遗漏&#xff0c;但博主已经尽了最大努力打破信息差&#xff0c;如果有遗漏还请见谅&#xff0c;嘻嘻&#xff0c;前路漫漫&#xff0c;我们一起前进&#xff01;&#xff01;&#xff0…

微信小程序--19(.wxml 模板文件简单归纳)

类似HTML用来描述当前页面的结构 一、普通样式 1.<view> 内容 </view> 二、滚波样式 1.<swiper> 内容 </swiper> 2.<swiper-item>滚波内容 </swiper-item> 3.常用属性 纵向&#xff1a;scroll-y横向&#xff1a;scroll-x圆点颜色&am…

LinuxC高级day03(Shell脚本)

【1】Shell脚本 1》Shell脚本基础概念 1> 概念 Shell使用方式&#xff1a;手动在命令行下命令或用Shell脚本 Shell脚本本质&#xff1a;Shell命令的有序集合 扩展名最好以 .sh 结尾&#xff0c;见名知义 也可以没有 Shell既是应用程序&#xff0c;又是一种脚本语言 解…

迁移学习之基本概念

迁移学习 1、通俗定义 迁移学习是一种学习的思想和模式 迁移学习作为机器学习的一个重要分支&#xff0c;侧重于将已经学习过的知识迁移应用于新的问题中 迁移学习的核心问题是&#xff0c;找到新问题和原问题之间的相似性&#xff0c;才可以顺利地实现知识地迁移 定义&…

运行pytorch报异常处理

一、问题现象及初步定位&#xff1a; 找不到指定的模块。 Error loading "D:\software\python3\Lib\site-packages\torch\lib\fbgemm.dll 此处缺少.dll文件&#xff0c;首先下载文件依赖分析工具 Dependencies https://github.com/lucasg/Dependencies/tree/v1.11.1 之后下…

leetcode169. 多数元素,摩尔投票法附证明

leetcode169. 多数元素 给定一个大小为 n 的数组 nums &#xff0c;返回其中的多数元素。多数元素是指在数组中出现次数 大于 ⌊ n/2 ⌋ 的元素。 你可以假设数组是非空的&#xff0c;并且给定的数组总是存在多数元素。 示例 1&#xff1a; 输入&#xff1a;nums [3,2,3] 输…

C# winform 三层架构 增删改查 修改数据(修改篇)

ss一.留言 本专栏三层架构已经更新了 添加 登录 显示&#xff0c;还差修改以及删除&#xff0c;本篇更新修改&#xff0c;主要操作为点击修改某一条数据&#xff0c;然后跳转页面进行修改。 二.展示 我们先看DAL代码 /// <summary>/// 修改/// </summary>/// &l…

【RTOS面试题】什么是抢占?抢占的原理、抢占的好处、抢占有什么局限性?

&#x1f48c; 所属专栏&#xff1a;【RTOS-操作系统-面试题】 &#x1f600; 作  者&#xff1a; 于晓超 &#x1f680; 个人简介&#xff1a;嵌入式工程师&#xff0c;专注嵌入式领域基础和实战分享 &#xff0c;欢迎咨询&#xff01; &#x1f496; 欢迎大家&#xf…

大语言模型的模型量化(INT8/INT4)技术

目录 一、LLM.in8 的量化方案 1.1 模型量化的动机和原理1.2 LLM.int8 量化的精度和性能1.3 LLM.int8 量化的实践 二、SmoothQuant 量化方案 2.1 SmoothQuant 的基本原理2.2 SmoothQuant 的实践 三、GPTQ 量化训练方案 3.1 GPTQ 的基本原理3.2 GPTQ 的实践 参考资料 一、LLM.i…

让对话AI帮助你做程序架构设计,以及解决你的疑问

我想问下对话AI,本文采取的是chatgpt免费版 我问&#xff1a; 你说程序的设计&#xff0c;前后端分离的BS架构。比如工人基础档案1000条记录&#xff0c;工程项目基础档案10条记录&#xff0c;其他相关这两个基础档案的具体功能&#xff0c;比如打卡记录&#xff0c;宿舍记录&…