介绍 ConcurrentHashMap

技术是为了解决问题而生的,ConcurrentHashMap 解决了多个线程同时操作一个 HashMap 时,可能出现的内部问题。当多个线程同时操作一个 HashMap 时,有可能会出现多线程同时修改一个共享变量(HashMap 类的成员变量),导致数据被覆盖,产生意想不到的错误。


ConcurrentHashMap 内部使用了锁和各种数据结构来保证访问 Map 是线程安全的。

ConcurrentHashMap 和 HashMap 底层的数组、链表结构几乎相同,底层对数据结构的操作思路是相同的。ConcurrentHashMap 除了数组 + 链表 + 红黑树的基本结构外,新增了转移节点结构(ForwardingNode)。


介绍转移节点(ForwardingNode)

转移节点是 ForwardingNode 结构, ForwardingNode 继承了 Node。ForwardingNode 节点的 hash 值固定为 -1。ForwardingNode 比 Node 多了一个 nextTable 成员变量,nextTable 成员变量的类型是 Node 数组。nextTable 成员变量是扩容之后的新数组。

如果数组在索引 i 上的结构是 ForwardingNode,那么表示这个哈希桶内的全部节点都已经转移到扩容之后的新数组,旧的哈希桶内的数据不能发生改变。转移节点(ForwardingNode)是为了保证 ConcurrentHashMap 扩容时的线程安全。保证了当一个哈希桶内的全部节点都已经转移到扩容之后的新数组后、扩容操作完成之前,旧的哈希桶内的数据不发生变化。

当一个哈希桶内的全部节点都已经转移到扩容之后的新数组后、扩容操作完成之前,如果有其他的线程执行 put 操作,需要将新增的节点 put 到旧的哈希桶内,那么这个线程会调用 helpTransfer() 方法帮助扩容。扩容完成之后,这个线程再将要新增的节点 put 到新的哈希桶内。

ConcurrentHashMap 的新增操作

ConcurrentHashMap 在 put 方法上对数据结构的操作思路和 HashMap 相同,但 ConcurrentHashMap 的 put() 方法写了很多保障线程安全的代码。当调用 ConcurrentHashMap 的 put() 方法时,put() 方法的处理逻辑如下:

  • 首先,如果 CHM 的成员变量 table 数组为空(null 或者 length 为 0),则调用 initTable() 方法初始化 table 数组。由于 initTable() 方法操作了共享变量,因此 initTable() 方法采用了一些手段来保证线程安全。
  • 接下来,它会调用 spread() 方法根据 key 计算出 hash 值,然后根据计算出的 hash 值计算出 key 对应的数组索引 i
  • 计算出 key 对应的数组索引 i 之后,它调用 tabAt() 方法,tabAt() 方法返回数组在索引 i 上的值。然后它根据数组在索引 i 上的值进行处理。由于 tabAt() 方法读取了共享变量 table 数组在索引 i 上的值,因此 tabAt() 方法调用 Unsafe 类的 get 方法保证数据的可见性
    • 如果数组在索引 i 上的值为 null,则直接生成一个新的节点,并调用 casTabAt() 方法让 tab[i] 指向该节点。由于 casTabAt() 方法操作了共享变量 tab 数组,因此 casTabAt() 方法调用 Unsafe 类的 compareAndSwap 方法保证数据不被覆盖
    • 如果数组在索引 i 上的值不为 null,则意味着需要解决 hash 冲突问题、扩容冲突问题。
  • 接上一步骤,如果数组在索引 i 上的值不为 null。
    • 如果索引 i 上的结构是转移节点(ForwardingNode 结构,节点的 hash 值为 -1,表明这个哈希桶内的全部节点都已经转移到扩容之后的新数组,旧的哈希桶内的数据不能发生改变),它就会调用 helpTransfer() 方法,helpTransfer() 方法会帮助扩容。扩容完成之后,它再将要新增的节点 put 到扩容之后的新数组中。
    • 如果索引 i 上的结构不是转移节点,那么它会使用 synchronized 关键字给索引 i 上的结构加锁,保证同时最多只有一个线程操作索引 i 上的结构。给索引 i 上的结构加锁之后,它会判断数组在索引 i 上的结构是链表 还是 红黑树,然后调用相应的新增代码。
      • 如果索引 i 上的结构是链表,则把新生成的节点加到链表的末尾;
      • 如果索引 i 上的结构是红黑树,那么使用红黑树方式新增。
  • 接上一步骤,如果索引 i 上的结构是普通链表,则把新生成的节点加到链表的末尾之后,需要判断是否需要将链表转为红黑树:
    • 如果链表的长度大于等于 8,并且数组的长度大于等于 64,则调用 treeifyBin() 将链表转为红黑树;
    • 如果链表的长度大于等于 8,但是数组的长度小于 64,则调用 tryPresize() 方法执行扩容操作;
    • 当红黑树中的节点个数小于等于 6 时,红黑树会转为链表。
  • 将节点加入 CHM 集合之后,put() 方法的最后一步是调用 addCount() 方法增加 ConcurrentHashMap 中元素个数的计数值。addCount() 方法的任务是:增加 ConcurrentHashMap 中元素的计数值。如果元素的数量超过了 ConcurrentHashMap 扩容的阈值(sizeCtl),那么就会调用 transfer() 方法执行扩容操作。如果此时有其他的线程已经在执行扩容操作,那么当前线程就协助扩容。

当调用 CHM 的 put() 方法时,如果 CHM 中已经存在要新增的 key,并且方法的入参 onlyIfAbsent 为 false,则替换旧值,并返回旧值。

ConcurrentHashMap 的扩容机制

ConcurrentHashMap 的扩容时机和 HashMap 相同,都是在 put() 方法的最后一步检查是否需要扩容。ConcurrentHashMap 扩容的方法叫做 transfer(),从 put() 方法的 addCount() 方法进去,就能找到 transfer() 方法。

如果 ConcurrentHashMap 中元素的数量超过了扩容的阈值(sizeCtl),那么它会调用 transfer() 方法执行扩容操作。ConcurrentHashMap 的扩容机制是扩容为原来容量的 2 倍。ConcurrentHashMap 扩容的处理逻辑和 HashMap 完全不同。


ConcurrentHashMap 扩容的大体思路如下:扩容需要把旧数组上的全部节点转移到扩容之后的新数组上,节点的转移是从数组的最后一个索引位置开始,一个索引一个索引进行的。每个线程一轮处理有限个数的哈希桶。当旧数组上的全部节点转移到扩容之后的新数组后,ConcurrentHashMap 的 table 成员变量指向扩容之后的新数组,扩容操作完成。transfer() 方法的处理逻辑如下:

  • 首先根据 CPU 核数和 table 数组的长度,计算当前线程一轮处理哈希桶的个数。ConcurrentHashMap 的 transferIndex 成员变量会记录下一轮 或者是 下一个线程要处理的哈希桶的索引值 + 1

    • 如果 CPU 核数为 1,那么当前线程一轮处理哈希桶的个数为 table 数组的长度;
    • 如果 CPU 核数大于 1,那么先计算 num = (tab.length >>> 3) / NCPU的值:
      • 如果 num 值大于等于 16,那么 num 值就是当前线程一轮处理哈希桶的个数;
      • 如果 num 值小于 16,那么当前线程一轮处理哈希桶的个数为 16。也就是说,线程一轮处理哈希桶的个数最小值为 16。
  • 领取完任务之后线程就开始处理哈希桶内的节点。节点的转移是从数组的最后一个索引位置开始,一个索引一个索引进行的。在转移索引 i 上的节点之前,它会使用 synchronized 关键字给索引 i 上的结构加锁,保证同时最多只有一个线程操作索引 i 上的结构。

  • 给索引 i 上的结构加锁之后,它会判断数组在索引 i 上的结构是链表 还是 红黑树,然后调用相应的节点转移代码。

    • 如果索引 i 上的结构是链表,它通过将节点 key 的 hash 值 和 数组的长度 n 做与运算获得 n 对应的二进制表示中的 1 这一位在 hash 值中是 0 还是 1,即 b = p.hash & n。获取到 b 之后,用 b 来判断要转移的节点是要挂到低位哈希桶里,还是挂到高位哈希桶里。遍历完链表,形成两个链表(低位链表、高位链表)之后,将链表的头节点赋值给对应的 tab[i]:

      • 如果 b 的值为 0,则要转移的节点挂到位哈希桶里
      • 如果 b 的值非 0,则要转移的节点挂到位哈希桶里
    • 如果索引 i 上的结构是红黑树那么使用红黑树方式转移节点。

  • 在将索引 i 上的全部节点转移到扩容之后的新数组后,它让旧数组 tab[i] 指向转移节点(ForwardingNode)。

  • 当旧数组上的全部节点转移到扩容之后的新数组后,ConcurrentHashMap 的 table 成员变量指向扩容之后的新数组,扩容操作完成。

介绍低位哈希桶、高位哈希桶:如果 ConcurrentHashMap 当前的数组长度为 n 时,一个节点的 key 对应的哈希桶索引为 i。那么 ConcurrentHashMap 扩容之后数组长度为 2n 时,这个节点的 key 对应的低位哈希桶的索引为 i,对应的高位哈希桶的索引为 i + n。


ConcurrentHashMap 支持多线程扩容:

  • 如果在扩容的过程中,有其他的线程执行新增操作,新增操作完成后,这个线程会调用 transfer() 方法协助扩容。
  • 如果一个线程在扩容时,有其他的线程执行新增操作,需要把节点 put 到索引为 i 的哈希桶内。其他的线程它发现索引 i 上的结构是转移节点(ForwardingNode 结构, 节点的 hash 值为 -1,表明这个哈希桶内的元素已经扩容迁移完成),那么这个线程它就会调用 helpTransfer() 方法,helpTransfer() 方法会调用 transfer() 帮助扩容。扩容完成之后,它再将要新增的节点 put 到扩容之后的新数组中。

ConcurrentHashMap 的查找操作

当调用 ConcurrentHashMap 的 get() 方法时,get() 方法的处理逻辑如下:

  • 首先,它会根据传入的 key 计算出 hash 值;然后根据计算出的 hash 值计算出 key 对应的数组索引 i
  • 计算出 key 对应的数组索引 i 之后,根据存储位置,从数组中取出对应的 Entry,然后通过 key 对象的 equals() 方法判断传入的 key 和 Entry 中的 key 是否相等:
    • 如果传入的 key 和 Entry 中的 key 相等,则查找操作完成,返回 Entry 中的 value;
    • 如果传入的 key 和 Entry 中的 key 不相等,则再判断数组在索引 i 上的结构是链表 还是 红黑树,然后调用相应的查找数据的方法。直到找到相等的 Entry 或者没有下一个 Entry 为止。

ConcurrentHashMap 的容量大小问题

ConcurrentHashMap 的数组长度总是为 2 的幂次方。不论传入的初始容量是否为 2 的幂次方,最终都会转化为 2 的幂次方。

ConcurrentHashMap 中根据 key 计算出 hash 值,然后根据计算出的 hash 值计算出 key 对应的数组索引 i:

  • 根据 key 计算处 hash 值:在计算 hash 值时,它先将 key 的 hashCode 值无符号右移 16 位,然后再和 key 的 hashCode 值做 异或 运算,即 hash = (hashCode >>> 16) ^ hashCode
  • 根据 hash 值计算出 key 对应的数组索引 i:在计算 key 对应的数组索引 i 时,它将 hash 值 和 数组的长度 - 1 做与运算获得 key 对应的数组索引 i,即 i = hash & (n - 1)

ConcurrentHashMap 的数组长度总是为 2 的幂次方设计的非常巧妙:

  • 在计算 hash 值时,它先将 key 的 hashCode 值无符号右移 16 位,然后再和 key 的 hashCode 值做 异或 运算,即 hash = (hashCode >>> 16) ^ hashCode。使 key 的 hashCode 值高 16 位的变化映射到低 16 位中,使 hashCode 值高 16 位也参与后续索引 i 的计算,减少了碰撞的可能性。
  • 在计算 key 对应的数组索引 i 时,它将 hash 值 和 数组的长度 - 1 做与运算获得 key 对应的数组索引 i,即 i = hash & (n - 1)。由于数组的长度 n 是 2 的幂次方,n - 1 可以保证它的二进制表示中的后几位都是 1,n 对应的二进制位及之前的位都是 0。因此,计算出的数组索引 i 和 hash 值的二进制表示中后几位有关,而与前面的二进制位无关
  • 当 b 是 2 的幂次方时,a % b == a & (b - 1)。CPU 处理位运算比处理数学运算的速度更快,效率更高。
  • 在 ConcurrentHashMap 扩容时,它通过将 key 的 hash 值 和 数组的长度 n 做与运算获得 n 对应的二进制表示中的 1 这一位在 hash 值中是 0 还是 1,即 b = p.hash & n。获取到 b 之后,用 b 来判断要转移的节点是要挂到低位哈希桶里,还是挂到高位哈希桶里:
    • 如果 b 的值为 0,则要转移的节点挂到位哈希桶里
    • 如果 b 的值非 0,则要转移的节点挂到位哈希桶里

ConcurrentHashMap 的计数

当调用 ConcurrentHashMap 的 put() 方法时,put() 方法的最后一步是调用 addCount() 方法。

addCount() 方法的任务是:增加 ConcurrentHashMap 中元素的计数值。如果元素的数量超过了 ConcurrentHashMap 扩容的阈值(sizeCtl),那么就会调用 transfer() 方法执行扩容操作。如果此时有其他的线程已经在执行扩容操作,那么当前线程就协助扩容。


ConcurrentHashMap 采用了一些数据结构和手段来支持高效的并发计数。ConcurrentHashMap 使用 long 类型的 baseCount 成员变量和 CounterCell 类型的 counterCells 数组来支持高效的并发计数。

  • baseCount 是基础的计数值。主要通过调用 Unsafe 类的 compareAndSwap 方法更新 baseCount 的值
  • counterCells 数组的使用:如果有多个线程调用 addCount() 方法增加元素的计数值,那么每个线程将要增加的计数值保存在 counterCells 数组中。当调用 ConcurrentHashMap 的 size() 方法获取元素个数时,size() 方法将循环遍历 counterCells 数组,累加计数值得到当时元素。

ConcurrentHashMap 的计数将线程竞争分散到 counterCells 数组的每一个元素,提高了并发计数的性能。

private transient volatile long baseCount;

// 如果 counterCells 数组不为空,则数组的长度为 2 的幂次方。
private transient volatile CounterCell[] counterCells; @sun.misc.Contended static final class CounterCell {
volatile long value;
CounterCell(long x) { value = x; }
}

Java集合 - ConcurrentHashMap的更多相关文章

  1. Java集合——ConcurrentHashMap

    集合是编程中最常用的数据结构.而谈到并发,几乎总是离不开集合这类高级数据结构的支持.比如两个线程需要同时访问一个中间临界区(Queue),比如常会用缓存作为外部文件的副本(HashMap).这篇文章主 ...

  2. Java集合---ConcurrentHashMap原理分析

    集合是编程中最常用的数据结构.而谈到并发,几乎总是离不开集合这类高级数据结构的支持.比如两个线程需要同时访问一个中间临界区(Queue),比如常会用缓存作为外部文件的副本(HashMap).这篇文章主 ...

  3. 2.Java集合-ConcurrentHashMap实现原理及源码分析

    一.为何用ConcurrentHashMap 在并发编程中使用HashMap可能会导致死循环,而使用线程安全的HashTable效率又低下. 线程不安全的HashMap 在多线程环境下,使用HashM ...

  4. 深入java集合系列文章

    搞懂java的相关集合实现原理,对技术上有很大的提高,网上有一系列文章对java中的集合做了深入的分析, 先转载记录下 深入Java集合学习系列 Java 集合系列目录(Category) HashM ...

  5. 死磕 java集合之ConcurrentHashMap源码分析(三)

    本章接着上两章,链接直达: 死磕 java集合之ConcurrentHashMap源码分析(一) 死磕 java集合之ConcurrentHashMap源码分析(二) 删除元素 删除元素跟添加元素一样 ...

  6. java集合之 ConcurrentHashMap的产生

    ConcurrentHashMap:  在java集合中 最常用的是ArrayList 效率最高的还是HashMap 但是线程不安全   HashTable是线程安全的(里面的方法是同步方法) 但相比 ...

  7. Java集合专题总结(1):HashMap 和 HashTable 源码学习和面试总结

    2017年的秋招彻底结束了,感觉Java上面的最常见的集合相关的问题就是hash--系列和一些常用并发集合和队列,堆等结合算法一起考察,不完全统计,本人经历:先后百度.唯品会.58同城.新浪微博.趣分 ...

  8. Java 集合框架

    Java集合框架大致可以分为五个部分:List列表,Set集合.Map映射.迭代器.工具类 List 接口通常表示一个列表(数组.队列.链表 栈),其中的元素 可以重复 的是:ArrayList 和L ...

  9. (转)Java集合框架:HashMap

    来源:朱小厮 链接:http://blog.csdn.net/u013256816/article/details/50912762 Java集合框架概述 Java集合框架无论是在工作.学习.面试中都 ...

  10. Java集合面试题

    1.Java集合框架是什么?说出一些集合框架的优点? 每种编程语言中都有集合,最初的Java版本包含几种集合类:Vector.Stack.HashTable和Array.随着集合的广泛使用,Java1 ...

随机推荐

  1. 深入理解独占锁ReentrantLock类锁

    ReentrantLock介绍 [1]ReentrantLock是一种基于AQS框架的应用实现,是JDK中的一种线程并发访问的同步手段,它的功能类似于synchronized是一种互斥锁,可以保证线程 ...

  2. Spring Cloud 整合 nacos 实现动态配置中心

    上一篇文章讲解了Spring Cloud 整合 nacos 实现服务注册与发现,nacos除了有服务注册与发现的功能,还有提供动态配置服务的功能.本文主要讲解Spring Cloud 整合nacos实 ...

  3. 如何在.NET程序崩溃时自动创建Dump?

    今天在浏览张队转载文章的留言时,遇到一个读者问了这样的问题,如下图所示: 首先能明确的一点是"程序崩溃退出了是不能用常规的方式dump的",因为整个进程树都已经退出.现场已经无法使 ...

  4. Burpsuite(科学版)安装教程

    前言 BurpSuite是一款用于攻击web 应用程序的集成平台,在安全圈被称作"抓包神器".本文主要讲解 BurpSuite破解版的安装教程. 配置环境变量 BurpSuite是 ...

  5. Day03.1:初学者安装IDEA后需要知道的小技巧

    初学者安装IDEA后需要知道的小技巧 1.输入psvm直接生成 main方法 2.输入sout可以直接生成输出语句 3.代码放大设置 4.注释颜色更改 5.代码字体大小通过Ctrl+鼠标滑轮控制的设置 ...

  6. jmeter时间戳

    时间戳这东西,在jmeter中会经常用到,自己又总是记不住,做个记录. jmeter自带的时间戳函数: ① ${__time(yyyy-MM-dd,)}  ,对应时间示例:2022-09-24 ② $ ...

  7. elasticsearch多字段聚合实现方式

    目录 1.背景 2.实现多字段聚合的思路 3.需求 4.数据准备 4.1 创建索引 4.2 准备数据 5.实现方式 5.1 multi_terms实现 5.1.1 dsl 5.1.2 java 代码 ...

  8. 2022春每日一题:Day 34

    题目:lowbit求和 (没有找到哪个公开题库有这个题) 题意:求数组中任意一对数的异或和的lowbit的总和. 对于异或,二进制位中两个数相等则为0,反之为1,而且此题是要求lowbit,那我们利用 ...

  9. FIT软件开发

    1.baidu,google 术和道 2.FIT: future integrated Technology 3.集体检视 > commiter 4.高内聚,低耦合 => 太极 => ...

  10. EdgeCore初学习

    ### 前提 初学edgeCore,有不足之处,欢迎指正 ### 大纲 1. 日志查看2. 重启3. 在线编译4. sftp同步代码5. 整体架构6. 通信协议7. 模拟实现(待实现) ### 步骤 ...