面试整理:HashMap\ConcurrentHashMap原来

一、为什么引入红黑树

链表过长时(时间复杂度为O(n)),搜索性能会很差,红黑树的搜索的时间复杂度为Olog(n);

二、红黑树转换时机

链表过长会转换成红黑树,转换要求:

  • 数组长度 >= 64, 数组长度小于64,会优先扩容数组
  • 链表节点数 >= 8

三、扩容

  • 创建一个新数组,长度是老数组的二倍。
  • 将老数组中的数据,迁移到新数组中。

四、扩容时机

  • 链表节点数 >= 8,数组长度 < 64
  • 当map中的元素个数超过阈值时,就会除非扩容。阈值 = 数组长度*负载因子
  • putAll方法时,如果写入的Map过大,会优先触发扩容,在将元素一个一个的写入

五、sizeCtl属性

表示三个信息

  • sizeCtl == -1:表示数组正在初始化
  • sizeCtl < -1:代表数组正在扩容
  • sizeCtl > 0:有两种情况
    • 数组未初始化,sizeCtl == 0代表数组没有指定具体的长度,采用默认的长度为16,sizeCtl > 0表示数组指定的初始化长度。
    • 数组已初始化,下次扩容的阈值(= 数组长度 * 负载因子)

六、 ConcurrentHashMap数组初始化

map创建出来后,数组不会立即被创建,而是随着一次的put操作,才会将数组创建出来。懒加载的效果。

在执行put操作时,会判断数组是否为null,或者长度是否为0,如果为null或者为0,那就去创建数组。

七、DCL机制保证初始化线程安全

cas

八、计算数组下标

i = (n - 1) & hashcode

本质就是:(数组长度 - 1) & (key的hashcode)

散列算法的本质就是让高位也能参与到计算索引位置的过程里,运算方式很简单:hashcode ^ (hashcode >>> 16)

散列算法的目的是为了减少哈希冲突,在CHM中,所谓的hash冲突就是出现了两个key不同,但是确认索引位置相同的情况,散列算法的本质就是让key的hashcode值的高低位做异或运算,让高位也参与到计算索引位置的过程中,从而减少hash冲突。

九、ConcurrentHashMap写入操作的并发安全

1.7 是通过 segment 实现的。

1.8 是锁的Node对象,通过cas+synchronized实现的。

十、计数器线程安全的实现方式

CHM没有直接引用LongAdder,LongAdder功能比较多,CHM不需要那么多,将一些核心代码复制到了CHM中,借鉴 LongAdder 的设计,通过"分治法"来解决高并发下的计数竞争问题

CHM中由成员变量baseCount,和CounterCell的数组里面的value,组成了多个存储元素个数的位置,当CAS操作时,可以选择任意位置去做++或者--的操作。

只是最后统计的时候,需要将baseCount跟CounterCell进行累加。

  1. 核心组件:BaseCount 与 CounterCell
    为了统计元素个数,ConcurrentHashMap 维护了两个核心变量:
    baseCount:基础计数器。
    在低并发(没有竞争)的情况下,线程会直接通过 CAS 操作更新这个变量。
    CounterCell[]:分散计数单元数组。
    在高并发(CAS 更新 baseCount 失败)的情况下,为了避免所有线程都去争抢 baseCount,系统会将计数压力分散到这个数组中。每个线程会绑定到数组中的某个元素(Cell)上进行更新。
  2. 统计流程:addCount(写入时)
    当你执行 put 或 remove 操作时,会调用 addCount 方法来更新总数。流程如下:
    尝试更新 baseCount:
    首先尝试通过 CAS 操作直接更新 baseCount。
    竞争失败 -> 降级到 CounterCell:
    如果 CAS 失败(说明有其他线程也在更新),或者 CounterCell 数组已经初始化,线程会尝试更新 CounterCell 数组中的某个位置。
    通常通过 ThreadLocalRandom.getProbe() & (数组长度-1) 来定位当前线程应该更新数组中的哪个格子。
    这就像是在银行办事,如果"总窗口"(baseCount)排长队,就分流到旁边的"分窗口"(CounterCell)去办理。
    扩容检查:
    在更新计数的同时,还会检查当前总数是否超过了扩容阈值,如果超过则触发扩容。
  3. 获取流程:sumCount(读取时)
    当你调用 size() 方法时,ConcurrentHashMap 会调用 sumCount() 方法来计算总数。
    计算公式:
    T
    o
    t
    a
    l

    b
    a
    s
    e
    C
    o
    u
    n
    t

(

C

o

u

n

t

e

r

C

e

l

l

.

v

a

l

u

e

)

Total=baseCount+∑(CounterCell[].value)

实现逻辑:

它会遍历 CounterCell 数组,将所有 Cell 的值累加到 baseCount 上。

  1. 关键特性:弱一致性

由于统计过程不加全局锁,size() 返回的结果是一个近似值(弱一致性)。

原因:在累加 baseCount 和 CounterCell 的过程中,可能又有其他线程完成了插入或删除操作。

结果:size() 返回的是调用时刻的一个估算值,可能略小于或略大于实际值,但在高并发场景下,这种微小的误差是可以接受的,换取的是极高的性能。

  1. 版本对比:JDK 1.7 vs JDK 1.8

表格

特性 JDK 1.7 (分段锁) JDK 1.8 (CAS + synchronized)

统计方式 遍历所有 Segment baseCount + CounterCell 数组

锁机制 需要尝试多次无锁统计,失败则锁住所有 Segment 全程无锁(CAS + 分散更新)

性能 数据量大时性能较差 极高,适合高并发

准确性 最终尝试加锁后可获得精确值 始终返回近似值(弱一致性)

总结

ConcurrentHashMap 的个数统计通过"化整为零"的策略,将原本集中的计数压力分散到多个 CounterCell 上,极大地降低了多线程环境下的竞争,从而实现了高效的并发统计。

相关推荐
夕除1 小时前
javaweb--16
java·状态模式
用户游民1 小时前
Android 腾讯X5WebView如何禁止系统自带剪切板和自定义剪切板视图
android·java
花月C1 小时前
Agent应用开发零基础入门:核心概念、环境配置与首次LLM调用
java·python
曹牧1 小时前
Java Web:DispatcherServlet
java·开发语言·前端
直奔標竿1 小时前
Java开发者AI转型第二十三课!Spring AI个人知识库实战(二):异步ETL流水线搭建与避坑指南
java·人工智能·spring boot·后端·spring
Lyyaoo.1 小时前
TreadLocal和TreadLocalMap
android·java·redis
AC赳赳老秦2 小时前
网安工程师提效:用 OpenClaw 实现漏洞扫描报告生成、安全巡检自动化、日志合规审计
java·开发语言·前端·javascript·python·deepseek·openclaw
ffqws_2 小时前
MyBatis 动态 SQL 详解:从原理到实战
java·sql·mybatis
浮尘笔记2 小时前
在Snowy后台无需编码实现自动化生成CRUD操作流程
java·开发语言·经验分享·spring boot·后端·程序人生·mybatis