ConcurrentHashMap的死循环问题

news2024/9/24 3:18:13

文章目录

  • 前言
  • 1. 情景复现
  • 2. 源码解析
  • 3. 代码调试
  • 4. 原因
  • 5. 解决


前言

对于ConcurrentHashMap来说,能保证多线程下的安全问题,但是在JDK1.8中还是会出现一个bug,就是computeIfAbsent,下面就来详细说说死循环的原因


1. 情景复现

首先就是bug的复现,首先了解下computeIfAbsent这个方法有什么用,其实方法第二个参数 lambda 表达式的意思就是如果找不到对应的key,那么就执行第二个方法,第二个方法的返回结果会作为value,和key一起存到 table 上面

abstract class Test {
    public static void main(String[] args) throws InterruptedException {
        System.out.println("方法开始");
        ConcurrentHashMap<String, String> map = new ConcurrentHashMap<>();
        System.out.println(map.computeIfAbsent("aaa", key -> {
            return "没找到aaa就返回我这个";
        }));
        System.out.println("方法结束 => " + map);
    }
}

在这里插入图片描述

然后下面就是整个bug的复现

abstract class Test {
    public static void main(String[] args) throws InterruptedException {
        System.out.println("方法开始");
        ConcurrentHashMap<String, String> map = new ConcurrentHashMap<>();
        map.computeIfAbsent("AaAa",
                key -> {
                    return map.computeIfAbsent("BBBB", key2->"BBBB");
                });
        System.out.println("方法结束 => " + map);
    }
}

执行结果:
在这里插入图片描述
上面就是这个bug的复现了,下面来到源码的解析流程



2. 源码解析

public V computeIfAbsent(K key, Function<? super K, ? extends V> mappingFunction) {
 		//首先会判断一下key和调用函数
        if (key == null || mappingFunction == null)
            throw new NullPointerException();
        //计算出hash值
        int h = spread(key.hashCode());
        V val = null;
        int binCount = 0;
        //for循环
        for (Node<K,V>[] tab = table;;) {
            Node<K,V> f; int n, i, fh;
            //1.当table为null的时候或者长度为0的时候
            if (tab == null || (n = tab.length) == 0)
            	//进行初始化
                tab = initTable();
            //2. 如果table已经初始化好了并且找到的下标位置是null,就去初始化这个位置
            else if ((f = tabAt(tab, i = (n - 1) & h)) == null) {
            	//设置一个占位的Node,表示这个位置已经被线程占了
            	//这个node的hash= -3
                Node<K,V> r = new ReservationNode<K,V>();
                //然后给这个节点加锁
                synchronized (r) {
                	//使用CAS把这个r添加到tab上面
                    if (casTabAt(tab, i, null, r)) {
                        binCount = 1;
                        Node<K,V> node = null;
                        try {
                        	//注意这里,添加占位节点之后会调用这个函数
                        	//因为我们根据key找不到,所以会调用这个函数
                            if ((val = mappingFunction.apply(key)) != null)
                            	//调用结果不为null,就创建一个node然后添加到table上面去
                                node = new Node<K,V>(h, key, val, null);
                        } finally {
                        	//设置到tab[i]的位置
                            setTabAt(tab, i, node);
                        }
                    }
                }
                //binCount记录添加的节点数
                if (binCount != 0)
                	//*******************注意第一个跳出循环的点在这里**************************
                    break;
            }
            //3. 如果该节点已经是MOVED状态,证明有其他线程正在进行扩容,当前节点被移除到新数组上面了,所以是MOVED
            else if ((fh = f.hash) == MOVED)
                tab = helpTransfer(tab, f);
            //4. 如果上面的都没有发生,进入下面的流程进行添加
            else {
                boolean added = false;
                //对tab节点进行加锁,粒度更小
                synchronized (f) {
                	//5. 再次判断是不是f,如果是,证明没有其他线程进行修改
                	//因为如果是树化或者其他操作有可能导致头结点被修改
                    if (tabAt(tab, i) == f) {
                    	//6. fh > 0, 说明是一个正常的节点,要执行正常节点的添加动作
                        if (fh >= 0) {
                        	...
                        }
                        //7. 判断是不是树节点,如果是树节点就用树的添加
                        else if (f instanceof TreeBin) {
                           ....
                        }
                    }
                }
                //8.判断是不是要树化了
                if (binCount != 0) {
                    if (binCount >= TREEIFY_THRESHOLD)
                        treeifyBin(tab, i);
                    if (!added)
                        return val;
                    //*******************注意第二个跳出循环的点在这里**************************
                    break;
                }
            }
        }
        //数量+1
        if (val != null)
            addCount(1L, binCount);
        return val;
    }

上面就是整个方法的一个解析的流程,注意上面的数组1-7,以及上面方法中两个能跳出循环的机会

  • 第一个跳出的机会是第一次进入的时候判断到要找的节点是null,这时候会调用 computeIfAbsent 的第二个参数,也就是调用方法得到返回值,把这个返回值put 到 table 对应的下标上面,就可以返回了。
  • 第二次跳出的机会是正常情况下添加到链表尾部或者添加到树上,然后 binCount 这个参数记录的添加的节点数 != 0,就可以退出,简单来说就是你要把节点能正常添加到 table 中

现在就开始分析这个流程 😉,首先是第一次进入 computeIfAbsent

  1. 首先进入 for 循环,for (Node<K,V>[] tab = table;; )
  2. 然后判断 if (tab == null || (n = tab.length) == 0 ) ,没问题,进行初始化
  3. 接下来再次进入 for 循环,判断 if (tab == null || (n = tab.length) == 0 ) 失效
  4. 然后判断 else if ((f = tabAt(tab, i = (n - 1) & h)) == null) 成功,此时根据hash找到了下标,下标的值是 null
  5. 接下来进入上面的else if 流程
    1. 创建一个占位节点,这个节点 hash 值为 -3,把这个节点使用 CAS 添加到 tab[f(下标)] 上面,成功,因为此时没有多线程竞争
    2. 重点来了,接下来会调用 mappingFunction.apply(key) 执行我们设置的第二个参数的方法

好了,到现在第一次的 computeIfAbsent 已经分析完成了,此时整个 table 状态就是:已经初始化完成,并且在 table[f] 节点处有一个占位节点,第一次的 computeIfAbsent 陷入等待状态,等待第二次 computeIfAbsent 返回结果

现在开始第二次 computeIfAbsent

  1. 首先进入 for 循环
  2. 然后判断 if (tab == null || (n = tab.length) == 0 ) ,已经初始化完了,继续向下走
  3. 判断 else if ((f = tabAt(tab, i = (n - 1) & h)) == null) 不成功,因为这时候已经被一个占位节点(hash = -3)占用了
  4. 判断 else if ((fh = f.hash) == MOVED) 失败,因为此时不是扩容状态
  5. 进入添加链表或者树的流程
    1. if (tabAt(tab, i) == f) 判断成功,这个 f 是下标的第一个节点,现在不是多线程肯定是成功的
    2. 判断 if (fh >= 0) 失败,fh 是 f 的 hash,此时 = -3
    3. 判断 else if (f instanceof TreeBin) 失败,因为不是树节点,只是普通链表节点
    4. 最后判断 if (binCount != 0) 失败,因为我们并没有完成节点插入工作,所以这里还是 0
  6. 到这里继续进行 for 循环


3. 代码调试

好了,到这里已经出现死循环了,原因就是第二次 computeIfAbsent 一直在死循环了,而第一次 computeIfAbsent 在等待第二次返回,导致了死循环。下面再来用代码调试一下:

  1. 进入方法,然后可以看到 AaAa 的 hash 值
    在这里插入图片描述

  2. 接下来初始化数组
    在这里插入图片描述

  3. 初始化完成之后再次进入 for 循环

  4. 判断 (f = tabAt(tab, i = (n - 1) & h)) == null 成功,要设置节点,然后调用 mappingFunction.apply(key) 第二次进入 computeIfAbsent 方法
    在这里插入图片描述

  5. 第二次进入 computeIfAbsent 方法,可以看到下面BBBB的 hash 值和 AaAa 是一样的
    在这里插入图片描述

  6. 判断 if (tab == null || (n = tab.length) == 0) 和 (f = tabAt(tab, i = (n - 1) & h)) == null 失败,下面是此时 table 上面 2031775 的节点
    在这里插入图片描述
    其实可以看出来这是一个ReservationNode节点,它的hash就是 -3

  7. 判断 else if ((fh = f.hash) == MOVED) 失败,MOVED = -1

  8. 进入最后一个else
    在这里插入图片描述

  9. 判断 if (tabAt(tab, i) == f) 成功
    在这里插入图片描述

  10. 判断 if (fh >= 0) 失败,fh = -3 这时候,fh是hash值

  11. 判断 else if (f instanceof TreeBin) 失败
    在这里插入图片描述

  12. 判断 if (binCount != 0) 失败,此时 binCount 还是 0
    在这里插入图片描述

  13. 再次进入循环
    在这里插入图片描述

好了,到这里已经调试完成了



4. 原因

其实到这里就演示完成了,之所以跳不出循环根本原因还是因为没办法把节点添加上 table

  • 错过了 table[i] = null 的添加,因为这时候第一次调用该方法的时候已经做了
  • 错过了 正常链表和正常树情况下的添加,因为第一次调用该方法把一个占位节点放到了 table[i] ,没办法进行添加,因为能添加的前提是头结点要是一个正常的节点
  • 第二次添加不了,不能返回结果给第一次的 computeIfAbsent 方法调用,

其实从上面的过程来看没什么问题,因为第一次 computeIfAbsent 在第二次 computeIfAbsent 没有返回结果之前肯定不能创建处一个真正的 Node 节点出来,只能把一个临时节点放到上面,意思就是告诉其他线程,这个位置我占了,只不过我现在还没有创建链表节点。



5. 解决

那我们来看看 JDK11是怎么解决的

public V computeIfAbsent(K key, Function<? super K, ? extends V> mappingFunction) {
        if (key != null && mappingFunction != null) {
            int h = spread(key.hashCode());
            V val = null;
            int binCount = 0;
            ConcurrentHashMap.Node[] tab = this.table;

            while(true) {
                int n;
                //初始化数组
                while(tab == null || (n = tab.length) == 0) {
                    tab = this.initTable();
                }

                ConcurrentHashMap.Node f;
                int i;
                ConcurrentHashMap.Node e;
                //判断找到的数组下标位置是不是一个null,如果是就添加一个占位节点
                if ((f = tabAt(tab, i = n - 1 & h)) == null) {
                    ConcurrentHashMap.Node<K, V> r = new ConcurrentHashMap.ReservationNode();
                    synchronized(r) {
                        if (casTabAt(tab, i, (ConcurrentHashMap.Node)null, r)) {
                            binCount = 1;
                            e = null;

                            try {
                            	//调用第二次computeIfAbsent
                                if ((val = mappingFunction.apply(key)) != null) {
                                    e = new ConcurrentHashMap.Node(h, key, val);
                                }
                            } finally {
                                setTabAt(tab, i, e);
                            }
                        }
                    }

                    if (binCount != 0) {
                        break;
                    }
                } else {
                    int fh;
                    if ((fh = f.hash) == -1) {
                    	//如果是 -1,就证明正在扩容,进入协助扩容流程
                        tab = this.helpTransfer(tab, f);
                    } else {
                        Object fk;
                        Object fv;
                        //判断第一个节点是不是我们要找的节点
                        if (fh == h && ((fk = f.key) == key || fk != null && key.equals(fk)) && (fv = f.val) != null) {
                            return fv;
                        }

                        boolean added = false;
                        synchronized(f) {
                            if (tabAt(tab, i) == f) {
                                if (fh < 0) {
                                    if (f instanceof ConcurrentHashMap.TreeBin) {
                                    	//树节点的处理
                                       	...
                                       //下面就是处理ReservationNode的流程,这里返回一个递归更新的异常错误
                                    } else if (f instanceof ConcurrentHashMap.ReservationNode) {
                                        throw new IllegalStateException("Recursive update");
                                    }
                                } else {
                                    ...
                                }
                            }
                        }
						//binCount就是添加的节点数目
                        if (binCount != 0) {
                            if (binCount >= 8) {
                                this.treeifyBin(tab, i);
                            }

                            if (!added) {
                                return val;
                            }
                            break;
                        }
                    }
                }
            }

            if (val != null) {
                this.addCount(1L, binCount);
            }

            return val;
        } else {
            throw new NullPointerException();
        }
    }

上面就是一个大概的流程,其实源码和 JDK8 差不多,但是在JDK11 专门对 fh < 0 进行了处理,并在里面判断 else if (f instanceof ConcurrentHashMap.ReservationNode),判断成功之后返回一个 “Recursive update” 的异常,Java 会认为如果出现这种情况,就发生了递归更新,所以就返回了一个异常。

说白了就是让你代码别这么写。

要是程序中需要用到 computeIfAbsent 的地方,要么就别嵌套调用,要么就先用key 查找一下,找不到就加入到 map中。总之递归不能出现。

下面就是JDK11中执行同样的测试用例的结果:
在这里插入图片描述





如果错误,欢迎指出!!!

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

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

相关文章

进阶C语言 第二章-------《进阶指针》 (指针数组、数组指针、函数指针、回调指针)知识点+基本练习题+深入细节+通俗易懂+完整思维导图+建议收藏

绪论 书接上回&#xff0c;通过对数据类型进阶的认识&#xff0c;你肯定对各种数据类型在内存中如何存储有了了解。虽然说&#xff0c;这方面可能对你的编程能力没什么进步。但是&#xff0c;他是一本内功秘籍&#xff0c;当我们遇到了这方面的问题时我们可以知道可能是哪一方面…

使用 Grafana 请求API接口

目的: 使用Grafana 配合JSON API 插件 请求API接口,完成可视化,实现一些简单的请求功能 假设我们想将如下的API接口返回的json数据可视化 这里借用一下 小熊同学的 金融数据接口 用请求如下接口举例 https://api.doctorxiong.club/v1/fund/detail?code000001&startDat…

色环电容读数方法要点总结

🏡《电子元器件学习目录》 目录 1,概述2,读数方法3,颜色对照表3.1,颜色与电容值数字对照关系表3.2,颜色与10的指数数字对照关系表3.3,颜色与误差对照关系表4,总结1,概述 本文简单介绍色环电容的读数方法。 2,读数方法 如下图所示色环电容共有四个色环。最粗的被命名…

36.Isaac教程--复合消息

复合消息 ISAAC教程合集地址: https://blog.csdn.net/kunhe0512/category_12163211.html 文章目录复合消息测量类型使用复合消息的示例手臂关节速度基本轨迹命令手臂关节和末端执行器命令CompositeMetric 和 CompositeAtlas组件使用 CompositeMetric 计算距离在 Python 脚本中创…

使用bookdown构建新年日记本

简介 一年多前在 B 站发布了一个视频&#xff0c;预览了一下基于 bookdown 构建的日记本。本打算之后更个文字版本教程。结果一直忘了&#xff0c;最近通过一位读者的了解&#xff0c;打算把这个坑补上。 本文内容将展示如何使用 bookdown 模板并修改成自己的日记本。此外&…

某阿里员工提问:年终绩效自己给自己打3.25,会有什么后果?网友回答:必死无疑,不要犯傻!...

年底评绩效&#xff0c;大多数人都会给自己打高绩效&#xff0c;但有些人却反其道而行之。最近&#xff0c;一位阿里员工提出了这样的问题&#xff1a;自评绩效时给自己打3.25&#xff0c;会有什么后果&#xff1f;希望领导能不按常理出牌&#xff0c;给自己3.75。有人问&#…

HashMap1.7中的线程安全问题

文章目录前言正文前言 下面聊聊JDK1.7HashMap的死循环问题&#xff0c;在这之前首先要知道JDK1.7的HashMap底层是数组 链表的形式的 正文 下面给出JDK1.7的扩容代码 //扩容代码 void resize(int newCapacity) {//旧的数组Entry[] oldTable table;//旧的数组长度int oldCap…

SpringBoot+Vue--引入Element-UI创建首页-笔记2

关于Vue项目目录结构介绍 https://tiantian.blog.csdn.net/article/details/128666429?spm1001.2014.3001.5502 先安装引入Element-UI Element - The worlds most popular Vue UI framework i是install的缩写 安装完成后,可以看到package.json里已经引入 在main.js里引入…

linux系统的结构

Linux系统一般有4个主要部分&#xff1a;内核、shell、文件系统和应用程序。内核、shell和文件系统一起形成了基本的操作系统结构&#xff0c;它们使得用户可以运行程序、管理文件并使用系统。内核内核是操作系统的核心&#xff0c;具有很多最基本功能&#xff0c;它负责管理系…

Java基础之《netty(31)—用netty实现RPC》

一、需求说明 1、dubbo底层使用了netty作为网络通讯框架&#xff0c;要求使用netty实现一个简单的RPC框架。 2、模仿dubbo&#xff0c;消费者和提供者约定接口和协议&#xff0c;消费者远程调用提供者的服务&#xff0c;提供者返回一个字符串&#xff0c;消费者打印提供者返回…

S32G274A spi发送数组值不变问题

官方例程的spi问题 spi发送函数 下面是S32G两个spi从机发送函数 /** * brief SPI/DSPI异步传输。 * 这个函数使用提供的总线参数初始化异步传输 , 通过外部设备。 * param[in] ExternalDevice -指向传输数据的外部设备的指针 * param[in] TxBuffer -发送缓冲区的指针。 * pa…

3.1动态规划--矩阵连乘问题

写在前面&#xff1a;矩阵连乘的要点 1、最优解数组的含义--A[1:n]的最少数乘次数 2、数组的填写方向--斜着填 3、递推方程含义 今天开始动态规划的学习&#xff0c;动态规划与分治法类似&#xff0c;基本思想就是将待求解的问题分成若干子问题&#xff0c;先求解子问题&am…

Java 23种设计模式(2.创建者模式-工厂设计模式)

代码分析 通过代码的不同实现方式&#xff0c;了解工厂模式 代码分析之后有具体的讲解 1.业务和逻辑分开实现 public class Operation {public static double GetResult(double numberA,double numberB,String operate){double result 0;switch (operate){case "":r…

SpringBoot+Vue项目月度员工绩效考核管理系统

文末获取源码 开发语言&#xff1a;Java 框架&#xff1a;springboot JDK版本&#xff1a;JDK1.8 服务器&#xff1a;tomcat7 数据库&#xff1a;mysql 5.7/8.0 数据库工具&#xff1a;Navicat11 开发软件&#xff1a;eclipse/myeclipse/idea Maven包&#xff1a;Maven3.3.9 浏…

Kubernetes:通过 kubectl 插件 kubectl-tree 查看API对象层级关系

写在前面 分享一个小工具 kubectl-tree&#xff0c;用于查看 k8s API 对象层级关系比如对于有状态应用来讲&#xff0c;可以看到Deployment --> ReplicaSet --> Pod 的构成关系博文内容涉及&#xff1a;tree 插件的安装以及使用。理解不足小伙伴帮忙指正 岂其食鱼&#x…

Java---微服务---Nacos安装

Nacos安装1.Windows安装1.1.下载安装包1.2.解压1.3.端口配置1.4.启动1.5.访问2.Linux安装2.1.安装JDK2.2.上传Nacos安装包2.3.解压2.4.端口配置2.5.启动2.6.访问3.Nacos的依赖1.Windows安装 开发阶段采用单机安装即可。 1.1.下载安装包 在Nacos的GitHub页面&#xff0c;提供…

Java/JavaScript有哪些图形图像处理的框架?

文章目录一个小问题引发的学习热潮其它几个图形库Eclipse GEF框架Java图像库JS 的图形框架图形处理库图像编辑物理引擎流程图/组织图/图编辑等全景图/AR/VR3D库Javascript游戏编程库尾声一个小问题引发的学习热潮 一直对Java图形图像编程念兹在兹&#xff0c;书架上有几本相关…

2、IDEA的卸载与安装

文章目录2、IDEA的卸载与安装2.1 卸载过程2.2 安装前的准备2.3 安装过程2.4注册过程方式一&#xff1a;免费试用30天方式二&#xff1a;官网购买方式三&#xff1a;教育使用2.5 闪退问题【尚硅谷】idea实战教程-讲师&#xff1a;宋红康 生活是属于每个人自己的感受&#xff0c;…

3.1 卷积神经网络的应用领域|卷积的作用|卷积特征值的计算方法|得到特征图表示|步长与卷积核大小对结果的影响|边缘填充方法

文章目录卷积神经网络的应用领域卷积的作用卷积特征值的计算方法得到特征图表示步长与卷积核大小对结果的影响边缘填充方法卷积神经网络的应用领域 检测任务分类与检索超分辨率重构医学任务无人驾驶NVIDIA Tegra X1&#xff08;显卡 GPU&#xff09; 卷积的作用 卷积神经网络…

产品设计-基础控件-信息输出控件

产品设计-基础控件-信息输出控件1.1 走马灯1.1.1 图片轮播样式1.1.2 文字轮播样式1.2 折叠面板1.3 时间轴与步骤条1.3.1 时间轴1.3.2 步骤条1.4标签和徽标1.4.1 标签和徽标1.4.2 徽标1.5 面包屑与查询器1.5.1 面包屑1.5.2 查询器1.6 列表页与详情页1.6.1 列表页1.6.2 详情页1.7…