ConcurrentHashMap 源码分析(一)

一、简述

本文对 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 权限设计(二)

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

在centos系统中使用boost库

打开MobaXterm软件 下载 boost_1_85_0.tar.gz tar -zxvf boost_1_85_0.tar.gz解压缩成boost_1_85_0文件夹 双击arrayDemo.cpp 在里面可以编写代码 arrayDemo.cpp #include <boost/timer/timer.hpp> #include <boost/array.hpp> #include <cmath> #inc…

Redis中的Lua脚本(六)

Lua脚本 清空repl_scriptcache_dict字典 每当主服务器添加一个新的从服务器时&#xff0c;主服务器都会清空自己的repl_scriptcache_dict字典&#xff0c;这是因为随着新从服务器的出现&#xff0c;repl_scriptcache_字典里面记录的脚本已经不再被所有从服务器载入过&#xf…

天梯赛 L2-052 吉利矩阵

//r[n]:当前第几列的值。 //l[n]:当前第几行的值。 暴力减止 #include<bits/stdc.h> using namespace std; #define int long long const int n1e3; int a,b,c,l[n],r[n],an; void dfs(int x,int y) {if(xb1){an;return ;}for(int i0;i<a;i){l[x]i;r[y]i;if(l[x]&l…

【001_音频开发-基础篇-专业术语】

001_音频开发-基础篇-专业术语 文章目录 001_音频开发-基础篇-专业术语创作背景术语表常见音源HDMI相关声音系统立体声2.1 声音系统5.1 环绕声系统5.1.2 环绕声系统7.1 环绕声系统7.1.4 环绕声系统9.1.4 环绕声系统 音质等级定义QQ音乐网易云音乐 创作背景 学历代表过去、能力…

ubuntu安装QEMU

qemu虚拟机的使用&#xff08;一&#xff09;——ubuntu20.4安装QEMU_ubuntu安装qemu-CSDN博客 遇到的问题&#xff1a; (1)本来使用git clone https://github.com/qemu/qemu.git fatal: 无法访问 https://github.com/qemu/qemu.git/&#xff1a;GnuTLS recv error (-110): …

IoT、IIoT、AIoT的区别是什么?

一、IoT、IIoT、AIoT的区别是什么&#xff1f; IoT、IIoT和AIoT都是物联网&#xff08;Internet of Things&#xff09;的不同应用和发展方向&#xff0c;但它们之间存在一些区别。 IoT&#xff08;物联网&#xff09;&#xff1a;物联网是指通过互联网连接各种物理设备&#x…

密码学 | 数字证书:应用

&#x1f951;原文&#xff1a;数字签名和数字证书的原理解读 - 知乎 &#x1f951;前文&#xff1a;密码学 | 数字签名 数字证书 - CSDN &#x1f951;提示&#xff1a;把客户端想成 Alice&#xff0c;服务器端想成 Bob 即可。客户端实际上指的是客户端浏览器。 下面&#…

openGauss学习笔记-267 openGauss性能调优-TPCC性能调优测试指导-网络配置-网卡多中断队列设置

文章目录 openGauss学习笔记-267 openGauss性能调优-TPCC性能调优测试指导-网络配置-网卡多中断队列设置267.1 操作步骤 openGauss学习笔记-267 openGauss性能调优-TPCC性能调优测试指导-网络配置-网卡多中断队列设置 本章节主要介绍openGauss数据库内核基于鲲鹏服务器和openE…

目标检测网络YOLO进化之旅

yolo系列网络在目标检测领域取得了巨大的成功&#xff0c; 尤其是在工程实践中&#xff0c; 以其出色的性能优势获得了广泛的应用落地。 YOLO的前3个版本是由同一个作者团队出品&#xff0c; 算是官方版本。 之后的版本都是各个研究团队自己改进的版本&#xff0c; 之间并无明…

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

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

设计模式——2_A 访问者(Visitor)

文章目录 定义图纸一个例子&#xff1a;如何给好奇宝宝提供他想知道的内容菜单、菜品和配方Menu(菜单) & Cuisine(菜品)Material(物料、食材) 产地、有机蔬菜和卡路里Cuisine & Material 访问者VisitorCuisine & Material 碎碎念访问者和双分派访问者和代理写在最后…

初学者如何选择ARM开发硬件?

1&#xff0e; 如果你有做硬件和单片机的经验,建议自己做个最小系统板&#xff1a;假如你从没有做过ARM的开发&#xff0c;建议你一开始不要贪大求全&#xff0c;把所有的应用都做好&#xff0c;因为ARM的启动方式和dsp或单片机有所不同&#xff0c;往往会碰到各种问题&#xf…

设计模式-创建型-抽象工厂模式-Abstract Factory

UML类图 工厂接口类 public interface ProductFactory {Phone phoneProduct();//生产手机Router routerProduct();//生产路由器 } 小米工厂实现类 public class XiaomiFactoryImpl implements ProductFactory {Overridepublic Phone phoneProduct() {return new XiaomiPhone…

使用 kubeadm 进行证书管理

使用 kubeadm 进行证书管理 一&#xff1a;使用 kubeadm 进行证书管理 1.检查证书是否过期 kubeadm certs check-expiration 2.手动续订证书 使用 kubeadm certs renew 命令 可以随时手动续订证书&#xff0c;该命令使用存储在/etc/kubernetes/pki中的 CA (or front-proxy-…

从零开始的vscode配置及安装rust教程

配置vscode的rust环境 下载安装vscodemac 环境1. 下载安装rust2. 配置 mac vscode环境3. 创建一个测试项目 windows 环境1. 安装c运行环境2. 安装配置rustup3. 配置windows vscode环境4. 创建一个测试项目 下载安装vscode 1.官网应用程序下载 vscode&#xff1a;https://code.v…

websocket 请求头报错 Provisional headers are shown 的解决方法

今日简单总结 websocket 使用过程中遇到的问题&#xff0c;主要从以下三个方面来分享&#xff1a; 1、前端部分 websocket 代码 2、使用 koa.js 实现后端 websocket 服务搭建 3、和后端 java Netty 库对接时遇到连接失败问题 一、前端部分 websocket 代码 <template>…

Spark和Hadoop的安装

实验内容和要求 1&#xff0e;安装Hadoop和Spark 进入Linux系统&#xff0c;完成Hadoop伪分布式模式的安装。完成Hadoop的安装以后&#xff0c;再安装Spark&#xff08;Local模式&#xff09;。 2&#xff0e;HDFS常用操作 使用hadoop用户名登录进入Linux系统&#xff0c;启动…

MyBatisPlus详解(二)条件构造器Wrapper、自定义SQL、Service接口

文章目录 前言2 核心功能2.1 条件构造器2.1.1 Wrapper2.1.2 QueryWrapper2.1.3 UpdateWrapper2.1.4 LambdaQueryWrapper 2.2 自定义SQL2.2.1 基本用法2.2.2 多表关联 2.3 Service接口2.3.1 IService2.3.1.1 save2.3.1.2 remove2.3.1.3 update2.3.1.4 get2.3.1.5 list2.3.1.6 co…

AlDente Pro for mac最新激活版:电池长续航软件

AlDente Pro是一款专为Mac用户设计的电池管理工具&#xff0c;旨在提供电池安全和健康管理的一站式解决方案。它具备实时监控电池状态的功能&#xff0c;让用户随时了解电池的电量、充电次数、健康状态等信息。 AlDente Pro for mac最新激活版下载 同时&#xff0c;AlDente Pro…

STM32 DA数字模拟转换原理

单片机学习&#xff01; 目录 前言 一、AD与DA 二、AD与DA硬件电路模型 三、运算放大器 四、运放电路 4.1 电压比较器 4.2 反相放大器 4.3 同相放大器 4.4 电压跟随器 五、DA原理 总结 前言 之前文章讲述了STM32中AD模拟数字转换器的内容&#xff0c;文中AD原理中涉及DA原理的内…