ConcurrentHashMap 源码分析(一)

news2024/11/13 7:59:06

一、简述

本文对 ConcurrentHashMap#put() 源码进行分析。

二、源码概览

public V put(K key, V value) {
    return putVal(key, value, false);
}

上面是 ConcurrentHashMap#put() 的源码,我们可以看出其核心逻辑在 putVal() 方法中。

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) {
            if (casTabAt(tab, i, null, new Node<K,V>(hash, key, value)))
                break;                   // no lock when adding to empty bin
        }
        else if ((fh = f.hash) == MOVED)
            tab = helpTransfer(tab, f);
        else if (onlyIfAbsent // check first node without acquiring lock
                 && fh == hash
                 && ((fk = f.key) == key || (fk != null && key.equals(fk)))
                 && (fv = f.val) != null)
            return fv;
        else {
            V oldVal = null;
            synchronized (f) {
                if (tabAt(tab, i) == f) {
                    if (fh >= 0) {
                        binCount = 1;
                        for (Node<K,V> e = f;; ++binCount) {
                            K ek;
                            if (e.hash == hash &&
                                ((ek = e.key) == key ||
                                 (ek != null && key.equals(ek)))) {
                                oldVal = e.val;
                                if (!onlyIfAbsent)
                                    e.val = value;
                                break;
                            }
                            Node<K,V> pred = e;
                            if ((e = e.next) == null) {
                                pred.next = new Node<K,V>(hash, key, value);
                                break;
                            }
                        }
                    }
                    else if (f instanceof TreeBin) {
                        Node<K,V> p;
                        binCount = 2;
                        if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
                                                              value)) != null) {
                            oldVal = p.val;
                            if (!onlyIfAbsent)
                                p.val = value;
                        }
                    }
                    else if (f instanceof ReservationNode)
                        throw new IllegalStateException("Recursive update");
                }
            }
            if (binCount != 0) {
                if (binCount >= TREEIFY_THRESHOLD)
                    treeifyBin(tab, i);
                if (oldVal != null)
                    return oldVal;
                break;
            }
        }
    }
    addCount(1L, binCount);
    return null;
}

上面是 ConcurrentHashMap#putVal() 的源码,有兴趣的小伙伴可以先试着读一下。

三、核心流程分析

final V putVal(K key, V value, boolean onlyIfAbsent) {
    
    // 检查 key 和 value 是否为 null,如果是则抛出 NullPointerException 异常
    if (key == null || value == null) throw new NullPointerException();
    
    // 调用 spread 方法将 key 的哈希码进行扩散,得到一个散列值 hash
    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;

        // 检查当前的哈希表(tab)是否为空或长度为 0
        if (tab == null || (n = tab.length) == 0)
            // 调用 initTable() 方法初始化哈希表
            tab = initTable();
            
        // 如果当前槽位(f = tabAt(tab, i = (n - 1) & hash))为空
        else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
            // 使用 CAS 操作尝试在该槽位上添加新的节点
            if (casTabAt(tab, i, null, new Node<K,V>(hash, key, value)))
                // 成功则跳出循环
                break;                   // no lock when adding to empty bin
        }
            
        // 如果当前槽位的哈希值为 MOVED
        else if ((fh = f.hash) == MOVED)
            // 帮助其进行哈希表的转移操作
            tab = helpTransfer(tab, f);
            
        // 如果 onlyIfAbsent 为 true,并且当前槽位的键与要添加的键相同
        else if (onlyIfAbsent // check first node without acquiring lock
                 && fh == hash
                 && ((fk = f.key) == key || (fk != null && key.equals(fk)))
                 && (fv = f.val) != null)
            // 直接返回当前槽位的值
            return fv;
            
        else {
            V oldVal = null;
            // 对当前槽位的节点进行加锁
            synchronized (f) {
                // 暂时省略 synchronized 中的内容
            }

            // 检查 binCount 是否不为 0
            if (binCount != 0) {
                // 如果 binCount 的值大于或等于 TREEIFY_THRESHOLD(默认值为8)
                if (binCount >= TREEIFY_THRESHOLD)
                    // 将当前的链表结构转化为红黑树结构 
                    treeifyBin(tab, i);
                // 如果 oldVal 不为 null
                if (oldVal != null)
                    // 直接返回 oldVal
                    return oldVal;
                // 跳出循环
                break;
            }
        }
    }
    // 调用 addCount 方法增加元素的数量
    addCount(1L, binCount);
    return null;
}

从上面的源码中可以分析出,ConcurrentHashMap#putVal() 的核心逻辑为:

在这里插入图片描述

  1. 首先进行空值检查,如果键或值为 null,那么抛出 NullPointerException
  2. 使用 spread() 方法,计算 Hash 值
  3. 开启循环,首先检测 Hash 表的状态是否已完成初始化
  4. 未完成初始化,使用 initTable() 方法完成初始化
  5. 若 Hash 表已完成初始化,则检查需要插入的槽位是否为空
  6. 若槽位为空,则采用 CAS 插入新节点,新节点插入成功退出循环
  7. 若槽位不为空,判断插入槽位是否需要移动
  8. 若需要移动,使用 helpTransfer() 方法实现槽位转移(扩容)
  9. 若不需要移动,则检查当前槽位的键是否与插入的键相同
  10. 若键相同,直接返回当前槽位的值,退出循环
  11. 若键不同,发生 Hash 冲突,进入 synchronized 代码块执行解决 Hash 冲突的逻辑

四、Hash 冲突流程分析

// 之前 synchronized 省略的内容
// 对哈希桶的节点加锁
synchronized (f) {
    // 检查当前的槽位是否改变
    if (tabAt(tab, i) == f) {
        // 如果当前节点是链表节点
        if (fh >= 0) {
            binCount = 1;
            // 遍历链表
            for (Node<K,V> e = f;; ++binCount) {
                K ek;
                // 如果找到了与添加的键相同的节点
                if (e.hash == hash &&
                    ((ek = e.key) == key ||
                     (ek != null && key.equals(ek)))) {
                    oldVal = e.val;
                    if (!onlyIfAbsent)
                        // 更新该节点的值
                        e.val = value;
                    break;
                }
                Node<K,V> pred = e;
                // 如果找到链表末尾依旧没有找到
                if ((e = e.next) == null) {
                    // 添加一个新的节点
                    pred.next = new Node<K,V>(hash, key, value);
                    break;
                }
            }
        }
        // 如果当前节点是红黑树节点
        else if (f instanceof TreeBin) {
            Node<K,V> p;
            binCount = 2;
            // 调用 putTreeVal() 方法
            if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
                                                  value)) != null) {
                oldVal = p.val;
                if (!onlyIfAbsent)
                    // 如果键已存在则更新旧值
                    p.val = value;
            }
        }
        // 如果当前节点是 ReservationNode
        else if (f instanceof ReservationNode)
            // 抛出 IllegalStateException 异常
            throw new IllegalStateException("Recursive update");
    }
}

上面是 ConcurrentHashMap#putVal() 方法中发生 Hash 冲突时的源码。

在这里插入图片描述

  1. 首先,将 Hash 桶中的节点加 synchronized
  2. 判断槽位是否改变
  3. 槽位未改变,检查是否为链表节点
  4. 若是链表节点,则遍历链表,键已存在则更新值,键不存在则新增节点
  5. 若是红黑树节点,则调用红黑树的 putTreeVal() 方法,键已存在则更新值,键不存在则新增节点
  6. 若是 ReservationNode 则抛出 IllegalStateException 异常

五、FAQ

5.1 helpTransfer() 方法是什么

helpTransfer() 方法的主要工作是检查是否满足扩容条件,如果满足,则协助进行扩容操作。具体来说,它会检查当前的哈希表是否正在进行扩容操作,如果是,则帮助完成扩容;如果不是,则直接返回当前的哈希表。

5.2 Hash 冲突源码中为什么需要判断槽位是否改变

if (tabAt(tab, i) == f) 的目的是为了检查在进入同步块之后,当前槽位的节点是否发生了变化。

在多线程环境下,当一个线程获取到锁并进入同步块时,其他线程可能已经修改了哈希表的状态。因此,在进行节点操作之前,需要再次检查当前槽位的节点是否与预期的节点相同。

如果当前槽位的节点与预期的节点不同,那么说明在这个线程获取锁的过程中,其他线程已经修改了哈希表的状态。在这种情况下,当前线程应该跳过后续的操作,因为它们可能基于错误的状态。
这是一种常见的并发编程技巧,被称为"双重检查锁"(Double-Checked Locking)。它可以确保在多线程环境下的正确性和效率。

5.3 ReservationNode 是什么

在 ConcurrentHashMap 中,ReservationNode 是一个特殊类型的节点,是一个临时的占位符,不应该出现在正常的操作中。如果出现了,那么可能是发生了递归更新

往期推荐

  1. IoC 思想简单而深邃
  2. ThreadLocal
  3. 终端的颜值担当-WindTerm
  4. Spring 三级缓存
  5. RBAC 权限设计(二)

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

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

相关文章

golang本地缓存库之bigcache

1. 前言 上周工作之余逛github看到一个本地缓存库bigcache&#xff0c;这个是allegro公司开源的一个项目&#xff0c;主要是用于本地缓存使用&#xff0c;根据他们的博客说明&#xff0c;他们编写这个库最初的目的就是实现一个非常快速的缓存服务。 看了下bigcache这个库的源…

前端入门:HTML(CSS边框综合案例)

案例&#xff1a; 源代码&#xff1a; css-borders.html: <body> <div id"square"> </div> <br> <div id"triangle"> </div> <br> <div id"trapezium"> </div> <br> <div id…

华为海思校园招聘-芯片-数字 IC 方向 题目分享——第六套

华为海思校园招聘-芯片-数字 IC 方向 题目分享——第六套 (共9套&#xff0c;有答案和解析&#xff0c;答案非官方&#xff0c;未仔细校正&#xff0c;仅供参考&#xff09; 部分题目分享&#xff0c;完整版获取&#xff08;WX:didadidadidida313&#xff0c;加我备注&#x…

Linux--Linux常用命令

Linux常用命令 前言Linux命令格式命令讲解1、ls:查看当前目录下所有的内容语法:ls[-al][dir]2、pwd: 查看当前所在目录3、cd : 切换目录4、touch[文件名] : 如果文件不存在新建文件5、mkdir: 创建目录6、rm: 删除指定文件7、rmdir: 删除空目录8、cat:用于显示文件内容9、m…

磁盘损坏无法读取:原因、恢复方案与防范之道

在数字化信息爆炸的时代&#xff0c;磁盘作为数据存储的重要载体&#xff0c;承载着无数重要的文件和资料。然而&#xff0c;当磁盘突然损坏&#xff0c;无法读取数据时&#xff0c;我们往往会陷入困境&#xff0c;焦虑不已。面对这种情况&#xff0c;我们该如何应对&#xff1…

python爬虫--------requests案列(二十七天)

兄弟姐们&#xff0c;大家好哇&#xff01;我是喔的嘛呀。今天我们一起来学习requests案列。 一、requests____cookie登录古诗文网 1、首先想要模拟登录&#xff0c;就必须要获取登录表单数据 登录完之后点f12&#xff0c;然后点击network&#xff0c;最上面那个就是登录接口…

ubuntu扩展根目录磁盘空间

ubuntu扩展根目录磁盘空间 扩展虚拟机磁盘空间 查看现有磁盘状态 查询现有分区状态&#xff0c;/dev/sda是我们要扩展的磁盘 fdisk -l 开始进行磁盘空间的扩容 parted /dev/sda#扩展3号分区的空间 resizepart 3刷新分区空间 resize2fs /dev/sda3查询扩展结果&#xff0c;…

BADI-AC_DOCUMENT-交货单过账科目替代

BADI-AC_DOCUMENT-交货单过账科目替代 一、业务场景 事务代码VL02N/VL22N及其他交货单过账事务&#xff0c;在交货单过账生成会计凭证的时候对科目进行替代 二、BADI增强&#xff1a;AC_DOCUMENT 这个BADI理论上可以处理很多的会计凭证科目替代&#xff0c;不止是交货单过账…

Redis入门到通关之数据结构解析-动态字符串SDS

文章目录 Redis数据结构-动态字符串动态扩容举例二进制安全SDS优点与C语言中的字符串的区别 Redis数据结构-动态字符串 我们都知道 Redis 中保存的Key是字符串&#xff0c;value 往往是字符串或者字符串的集合。可见字符串是 Redis 中最常用的一种数据结构。 不过 Redis 没有…

Django中间件的源码解析流程(上)——中间件载入的前置

目录 1. ​前言​ 2. 请求的入口 3. 中间件加载的入口 4. 源码中的闭包实现 5. 最后 1. 前言 哈喽&#xff0c;大家好&#xff0c;我是小K,今天咋们分享的内容是&#xff1a;在学会Django中间件之后&#xff0c; 我们继续深入底层源码。 在执行中间件时请求到来总是从前往后…

Golang那些违背直觉的编程陷阱

目录 知识点1&#xff1a;切片拷贝之后都是同一个元素 知识点2&#xff1a;方法集合决定接口实现&#xff0c;类型方法集合是接口方法集合的超集则认定为实现接口&#xff0c;否则未实现接口 切片拷贝之后都是同一个元素 package mainimport ("encoding/json"&quo…

[Kubernetes] etcd的集群基石作用

文章目录 1. 配置存储2. 数据一致性3. 服务发现与协调4. 集群状态中枢5. 集群稳定性 1. 配置存储 etcd作为一个高度可靠的分布式键值存储系统&#xff0c;存储了Kubernetes集群的完整配置和状态数据。集群的元数据&#xff0c;包括节点信息、命名空间、部署、副本集、服务、持…

【leetcode面试经典150题】65. 旋转链表(C++)

【leetcode面试经典150题】专栏系列将为准备暑期实习生以及秋招的同学们提高在面试时的经典面试算法题的思路和想法。本专栏将以一题多解和精简算法思路为主&#xff0c;题解使用C语言。&#xff08;若有使用其他语言的同学也可了解题解思路&#xff0c;本质上语法内容一致&…

Mac电池管理软件 Batteries for Mac v2.2.9直装版

Batteries for Mac&#xff0c;作为一款专为Mac用户设计的电池管理软件&#xff0c;以其强大的功能和智能的监测机制&#xff0c;为用户提供了便捷、高效的电池使用体验。 Batteries for Mac(Mac电池)v2.2.9直装版下载 首先&#xff0c;Batteries for Mac具备实时电池监测功能&…

在protobuf里定义描述rpc方法的类型

service UserServiceRpc //在test.proto中定义 { rpc Login(LoginRequest)returns(LoginResponse); rpc GetFriendLists(GetFriendListRequest)returns(GetFriendListResponse); } test.proto文件生成test.pb.cc protoc test.proto --cpp_out./ 将生成的…

前端工程化01-复习jQuery当中的AJAX

4.1、基础概念 什么是服务器 一台存储网站内容、网站文件的电脑 什么是资源 网站中使用的文件&#xff08;html、css、图片、…&#xff09;这些东西就叫做资源数据也是服务器上的资源&#xff0c;而且是一个网站的灵魂 客户端 客户端应该指上网的设备但是在前端开发中&a…

微软如何打造数字零售力航母系列科普01 --- Azure顾问(AZURE Advisor)简介

Azure顾问&#xff08;AZURE Advisor&#xff09;简介 目录 一、什么是AZURE顾问&#xff08;AZURE Advisor&#xff09;&#xff1f; 二、常见问题 三、接下来的步骤 一、什么是AZURE顾问&#xff1f; AZURE顾问是一种数字云助手&#xff0c;可帮助您遵循最佳实践来优化Az…

【QT进阶】Qt http编程之用户登录注册功能实现

往期回顾 【QT进阶】Qt http编程之http与https简单介绍-CSDN博客 【QT进阶】Qt http编程之后端API测试工具postman使用介绍-CSDN博客 【QT进阶】Qt http编程之http相关类的简单介绍-CSDN博客 【QT进阶】Qt http编程之用户登录注册功能实现 一、最终效果展示 重点在逻辑实现&a…

分类神经网络2:ResNet模型复现

目录 ResNet网络架构 ResNet部分实现代码 ResNet网络架构 论文原址&#xff1a;https://arxiv.org/pdf/1512.03385.pdf 残差神经网络(ResNet)是由微软研究院的何恺明、张祥雨、任少卿、孙剑等人提出的&#xff0c;通过引入残差学习解决了深度网络训练中的退化问题&#xff…

[Spring Cloud] (4)搭建Vue2与网关、微服务通信并配置跨域

文章目录 前言gatway网关跨域配置取消微服务跨域配置 创建vue2项目准备一个原始vue2项目安装vue-router创建路由vue.config.js配置修改App.vue修改 添加接口访问安装axios创建request.js创建index.js创建InfoApi.js main.jssecurityUtils.js 前端登录界面登录消息提示框 最终效…