当前位置: 首页 > news >正文

HashMap 与 ConcurrentHashMap 深度解析

文章目录

    • 一、HashMap 核心原理
      • 1. 数据结构演进
      • 2. 核心参数
      • 3. 哈希算法优化
      • 4. put() 流程详解
      • 5. 扩容机制(resize)
    • 二、ConcurrentHashMap 并发实现
      • 1. JDK 版本演进
      • 2. JDK1.8 核心机制
        • 2.1 关键数据结构
        • 2.2 并发控制技术
      • 3. put() 并发流程
        • 3.1. 无锁CAS操作
        • 3.2. 细粒度锁(哈希冲突处理)
        • 3.3. 多线程协作扩容
      • 4. 并发扩容机制
      • 5. size() 精确计数
    • 三、高频面试题深度解析
      • 1. HashMap 线程安全问题及表现
      • 2. 为什么链表长度超过8转红黑树?
      • 3. ConcurrentHashMap 读操作为什么不需要加锁?
      • 4. ConcurrentHashMap 的 size() 是精确值吗?
      • 5. Key 的设计要求?
    • 四、性能优化实践
      • 1. HashMap 初始化优化
      • 2. ConcurrentHashMap 复合操作安全
    • 五、JDK 新特性
      • 1. JDK8 新增方法
    • 六、经典问题对比总结

一、HashMap 核心原理

在这里插入图片描述

1. 数据结构演进

  • JDK1.7:数组 + 链表
  • JDK1.8+:数组 + 链表 + 红黑树(链表长度 ≥ 8 且数组长度 ≥ 64 时树化)
// JDK 17 节点定义
static class Node<K,V> implements Map.Entry<K,V> {final int hash;final K key;V value;Node<K,V> next; // 链表指针
}static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {TreeNode<K,V> parent;  // 红黑树父节点TreeNode<K,V> left;    // 左子节点TreeNode<K,V> right;   // 右子节点TreeNode<K,V> prev;    // 前驱节点(删除时快速定位)boolean red;           // 颜色标记
}

2. 核心参数

参数默认值说明
DEFAULT_INITIAL_CAPACITY16默认初始容量
MAXIMUM_CAPACITY1 << 30最大容量 (1073741824)
DEFAULT_LOAD_FACTOR0.75f负载因子(扩容阈值比例)
TREEIFY_THRESHOLD8链表转红黑树阈值
UNTREEIFY_THRESHOLD6红黑树转链表阈值
MIN_TREEIFY_CAPACITY64最小树化容量

3. 哈希算法优化

static final int hash(Object key) {int h;return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
  • 高位扰动hashCode() 高16位与低16位异或,减少哈希碰撞
  • 索引计算(n - 1) & hash 替代取模运算

4. put() 流程详解

计算 key 的 hash
桶是否为空
直接插入新节点
首节点是否匹配
覆盖值
是否为树节点
红黑树插入
遍历链表
找到相同key?
覆盖值
尾插法添加节点
链表长度>=8?
数组长度>=64?
树化
扩容
重新哈希
public V put(K key, V value) {return putVal(hash(key), key, value, false, true);}    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict) {// tab:指向哈希桶数组的引用;// p:当前定位到的桶中的节点;// n:哈希桶数组长度;// i:键的哈希值计算出的桶下标Node<K,V>[] tab; Node<K,V> p; int n, i;// 初始化哈希桶数组(懒加载),未初始化则调用 resize() 初始化数组并获取新长度 nif ((tab = table) == null || (n = tab.length) == 0)n = (tab = resize()).length;// 如果目标桶为空,直接创建新节点放入该桶if ((p = tab[i = (n - 1) & hash]) == null)tab[i] = newNode(hash, key, value, null);else {// 目标桶不为空,表示发生了哈希冲突,分三种情况Node<K,V> e; K k;// 情况1:桶的首节点就是目标键if (p.hash == hash &&((k = p.key) == key || (key != null && key.equals(k))))// 标记已存在节点e = p;// 情况2:桶中是红黑树节点else if (p instanceof TreeNode)// putTreeVal在树中插入/查找e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);// 情况3:桶中是链表else {for (int binCount = 0; ; ++binCount) {if ((e = p.next) == null) {// 到达链表尾部:插入新节点p.next = newNode(hash, key, value, null);// 检查是否需树化(链表长度≥8)if (binCount >= TREEIFY_THRESHOLD - 1)treeifyBin(tab, hash);break;}// 链表中找到相同键if (e.hash == hash &&((k = e.key) == key || (key != null && key.equals(k))))break;// 继续遍历p = e;}}//键已存在if (e != null) {V oldValue = e.value;// 允许覆盖值 或 原值为null时更新if (!onlyIfAbsent || oldValue == null)e.value = value;afterNodeAccess(e);// 返回旧值return oldValue;}}// 修改次数+1(用于快速失败机制)++modCount;// 超过扩容阈值if (++size > threshold)// 扩容哈希表resize();// 回调方法(LinkedHashMap使用)afterNodeInsertion(evict);return null;}

5. 扩容机制(resize)

链表处理
数据迁移
容量计算
单节点
红黑树
链表
计算: (e.hash & oldCap)==0?
加入低位链表
加入高位链表
低位链表→原位置
高位链表→原位置+旧容量
遍历每个桶
桶非空?
清空旧桶(GC)
桶内节点类型
直接计算新位置: hash & (newCap-1)
调用split()拆分树
构建高低位链表
旧容量>0?
达到最大容量?
新容量=旧容量*2
计算新阈值(双倍或公式)
旧阈值>0?
新容量=旧阈值
使用默认容量/阈值
resize方法
记录旧表/容量/阈值
阈值=Integer.MAX_VALUE, 返回旧表
创建新表
旧表非空?
结束迁移
返回新表
    final Node<K,V>[] resize() {// 1.扩容// 当前哈希桶数组Node<K,V>[] oldTab = table;// 当前容量int oldCap = (oldTab == null) ? 0 : oldTab.length;// 当前扩容阈值int oldThr = threshold;// 新容量、新阈值(初始0)int newCap, newThr = 0;// 情况1:当前表已初始化(扩容操作)if (oldCap > 0) {// 容量已达上限(1<<30)if (oldCap >= MAXIMUM_CAPACITY) {// 阈值设为int最大值threshold = Integer.MAX_VALUE;// 直接返回原表,不再扩容return oldTab;}// 正常扩容:新容量=旧容量*2else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&oldCap >= DEFAULT_INITIAL_CAPACITY)// 新阈值=旧阈值*2newThr = oldThr << 1; }// 新容量=构造时计算的阈值else if (oldThr > 0) newCap = oldThr;else {               // 默认容量16newCap = DEFAULT_INITIAL_CAPACITY;// 16*0.75=12newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);}if (newThr == 0) {// 新容量*负载因子float ft = (float)newCap * loadFactor;newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?(int)ft : Integer.MAX_VALUE);}// 更新全局阈值threshold = newThr;@SuppressWarnings({"rawtypes","unchecked"})Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];// 更新全局table引用table = newTab;// 2.数据迁移(重哈希)if (oldTab != null) {// 遍历每个桶for (int j = 0; j < oldCap; ++j) {Node<K,V> e;// 桶中有数据if ((e = oldTab[j]) != null) {// 清空旧桶(帮助GC)oldTab[j] = null;// 情况1:桶中只有一个节点if (e.next == null)// 直接放入新桶newTab[e.hash & (newCap - 1)] = e;// 情况2:红黑树节点else if (e instanceof TreeNode)// 拆分红黑树((TreeNode<K,V>)e).split(this, newTab, j, oldCap);// 情况3:链表节点(保持顺序迁移)else { // 低位链表Node<K,V> loHead = null, loTail = null;// 高位链表Node<K,V> hiHead = null, hiTail = null;Node<K,V> next;do {// 通过 hash & oldCap 判断位置next = e.next;if ((e.hash & oldCap) == 0) {if (loTail == null)loHead = e;elseloTail.next = e;loTail = e;}else {if (hiTail == null)hiHead = e;elsehiTail.next = e;hiTail = e;}} while ((e = next) != null);// 低位链表放在原索引位置if (loTail != null) {loTail.next = null;newTab[j] = loHead;}// 高位链表放在 [原索引+oldCap] 位置if (hiTail != null) {hiTail.next = null;newTab[j + oldCap] = hiHead;}}}}}return newTab;}
  • 位置重计算e.hash & oldCap == 0 保持原位,否则移动到 原位置 + oldCap
  • 链表拆分:保持原有顺序(JDK1.8优化)

二、ConcurrentHashMap 并发实现

1. JDK 版本演进

版本实现方式锁粒度并发度限制
JDK1.7分段锁(Segment)段级别最大并发数=段数
JDK1.8CAS + synchronized桶级别(首节点)无上限

2. JDK1.8 核心机制

2.1 关键数据结构
transient volatile Node<K,V>[] table; // volatile 保证可见性
private transient volatile int sizeCtl; // 控制标识符
2.2 并发控制技术
  • CAS:用于无锁化初始化、计数更新等
  • synchronized:锁定桶的首节点(细粒度锁)
  • volatile:保证数组引用的可见性
  • ForwardingNode:扩容时标记迁移完成的桶

3. put() 并发流程

final V putVal(K key, V value, boolean onlyIfAbsent) {// 1. 计算hashfor (Node<K,V>[] tab = table;;) {Node<K,V> f; int n, i, fh;// 2. 延迟初始化if (tab == null || (n = tab.length) == 0)tab = initTable();// 3. CAS尝试无锁插入else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {if (casTabAt(tab, i, null, new Node<K,V>(hash, key, value)))break;}// 4. 检测到扩容中,协助扩容else if ((fh = f.hash) == MOVED)tab = helpTransfer(tab, f);// 5. 锁住桶首节点else {synchronized (f) {if (tabAt(tab, i) == f) {// 6. 链表或树插入}}}}// 7. 计数更新(LongAdder思想)addCount(1L, binCount);
}
3.1. 无锁CAS操作
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {if (casTabAt(tab, i, null, new Node<K,V>(hash, key, value)))break;
}
  • 原理
    • 通过tabAt()获取最新桶状态(使用volatile读)
    • 当桶为空时,用CAS原子操作设置新节点
    • 失败时重试(自旋)
  • 优势:避免锁开销,实现最高效的空桶插入
3.2. 细粒度锁(哈希冲突处理)
synchronized (f) {if (tabAt(tab, i) == f) {// 链表/树操作}
}
  • 防护机制:桶级别锁(而非整个map)
  • 关键点
    • 只锁定当前桶的首节点f
    • 二次检查tabAt(tab, i) == f(防止锁前状态变化)
    • 不同桶的操作完全并行
  • 优势:将锁竞争范围从全局缩小到单个桶
3.3. 多线程协作扩容
else if ((fh = f.hash) == MOVED)tab = helpTransfer(tab, f);
  • 防护机制:状态检测+协作扩容
  • 工作原理
    • 当节点hash=MOVED(-1)表示桶正在迁移
    • 当前线程调用helpTransfer()参与数据迁移
    • 多线程共同完成扩容任务
  • 优势化阻塞为协作,加速扩容过程

4. 并发扩容机制

  • 多线程协作:每个线程处理固定步长(stride)的桶
  • 迁移标记:ForwardingNode 标记已迁移的桶
  • 扩容触发
    • 链表转树时检测容量
    • 新增节点后检测总节点数

5. size() 精确计数

// 使用 CounterCell 数组分散计数
@sun.misc.Contended static final class CounterCell {volatile long value;CounterCell(long x) { value = x; }
}final long sumCount() {CounterCell[] as = counterCells;long sum = baseCount;if (as != null) {for (CounterCell a : as) {if (a != null)sum += a.value;}}return sum;
}

三、高频面试题深度解析

1. HashMap 线程安全问题及表现

问题类型JDK1.7 表现JDK1.8 表现
数据覆盖✅ 存在(hash碰撞时)✅ 存在(hash碰撞时)
死循环✅ 扩容时链表反转导致❌ 已修复(尾插法)
扩容丢失数据✅ 并发扩容可能导致✅ 仍可能(需外部同步)

解决方案

// 1. 使用 Collections.synchronizedMap
Map<String, String> syncMap = Collections.synchronizedMap(new HashMap<>());// 2. 使用 ConcurrentHashMap(推荐)
Map<String, String> safeMap = new ConcurrentHashMap<>();

2. 为什么链表长度超过8转红黑树?

  • 概率依据:根据泊松分布,哈希冲突达到8的概率小于千万分之一
  • 性能权衡
    • 链表查询:O(n)
    • 红黑树查询:O(log n)
  • 阈值选择:8是经验值,平衡转换成本和性能收益

3. ConcurrentHashMap 读操作为什么不需要加锁?

static final <K,V> Node<K,V> tabAt(Node<K,V>[] tab, int i) {return (Node<K,V>)U.getObjectVolatile(tab, ((long)i << ASHIFT) + ABASE);
}
  • volatile 读:通过 Unsafe.getObjectVolatile 保证可见性
  • 不变性保证:Node 的 key 和 hash 声明为 final
  • 扩容安全:读操作遇到 ForwardingNode 会转到新表查询

4. ConcurrentHashMap 的 size() 是精确值吗?

  • JDK1.7:分段计算,可能不是绝对精确(锁分段时变化)
  • JDK1.8:基于 CounterCellLongAdder 实现,理论上是精确值
    • 实际使用中可作为精确值使用
    • 高并发下可能有短暂延迟

5. Key 的设计要求?

  • 不可变性:推荐使用不可变对象(如 String、Integer)

    // 错误示例:可变Key导致哈希值变化
    class MutableKey {int id;// 省略hashCode和equals
    }MutableKey key = new MutableKey(1);
    map.put(key, "A");
    key.id = 2; // 修改后无法再通过原key访问
    
  • 重写 hashCode() 和 equals() 的契约:

    • 相等对象必须有相同 hashcode
    • 不相等的对象尽量产生不同的 hashcode

四、性能优化实践

1. HashMap 初始化优化

// 已知元素数量时避免多次扩容
int expectedSize = 10000;
Map<String, Integer> map = new HashMap<>((int)(expectedSize / 0.75f) + 1);

2. ConcurrentHashMap 复合操作安全

// 非原子操作:先检查后执行(存在竞态条件)
if (!map.containsKey(key)) {map.put(key, value); // 可能被其他线程覆盖
}// 解决方案1:使用 putIfAbsent
map.putIfAbsent(key, value);// 解决方案2:使用 compute 原子方法
map.compute(key, (k, v) -> v == null ? newValue : v);

五、JDK 新特性

1. JDK8 新增方法

// 1. 原子更新
map.compute("key", (k, v) -> v == null ? 1 : v + 1);// 2. 键值对遍历
map.forEach(2, (k, v) -> System.out.println(k + ": " + v), entry -> entry.getValue() > 100
);// 3. 搜索操作
String result = map.search(1, (k, v) -> v.startsWith("A") ? k : null
);

六、经典问题对比总结

特性HashMapConcurrentHashMap
线程安全✅ (CAS + synchronized)
锁粒度无锁(非线程安全)桶级别(首节点)
Null支持✅ key/value可为null❌ 禁止null键值(避免歧义)
迭代器一致性Fail-Fast弱一致性
性能开销最低读接近HashMap,写有额外开销
适用场景单线程环境高并发环境

注意事项

  1. 在单线程场景优先使用 HashMap
  2. 并发场景使用 ConcurrentHashMap,避免使用 Hashtable
  3. 分布式场景考虑 Redis 等分布式缓存
http://www.dtcms.com/a/317781.html

相关文章:

  • Java Stream (二)
  • 【模电笔记】—— 直流稳压电源——稳压电路
  • 从“T+1”到“T+0”:基于SQL构建MES到数据仓库的数据采集通道
  • 嵌入式学习---在 Linux 下的 C 语言学习 Day9
  • 时隔六年!OpenAI 首发 GPT-OSS 120B / 20B 开源模型:性能、安全与授权细节全解
  • PDW分选如何展示到界面上
  • MCU控制ADAU1701,用System Workbench for STM32导入工程
  • 力扣137:只出现一次的数字Ⅱ
  • 周志华院士西瓜书实战(二)MLP+SVM+贝叶斯分类器+决策树+集成学习
  • 一周学会Matplotlib3 Python 数据可视化-图形的组成部分
  • 1.1.3 本专栏内容介绍
  • 气象可视化网站整理:探索天气数据的视觉呈现
  • CRMEB会员电商系统集群部署 + 腾讯云日志托管优化方案
  • 实名认证 —— 腾讯云驾驶证识别接口
  • 云市场周报 (2025.08.06):腾讯云开学季、阿里云OSS降价及市场格局分析
  • Spring文件泄露与修复方案总结
  • C语言零基础第12讲:各类指针变量介绍与转移表应用
  • 基于Github Pages搭建个人博客站点:hexo环境搭建、本地预览与发布
  • 11、系统配置System文件夹
  • Docker 数据存储路径(解决默认docker路径位置磁盘空间不足的情况)
  • React(二):jsx事件绑定、条件渲染、列表渲染、jsx的本质、购物车案例
  • 玳瑁的嵌入式日记D13-0806(C语言)
  • Spring Boot部门管理系统:查询、删除、新增实战
  • IntelliJ IDEA 2025.1.4.1 中文版
  • Nacos配置中心和数据隔离在idea中的实现
  • NWinfo(硬件信息检测工具)v1.4.20绿色免费版,U盘随走随检,结果即刻导出
  • 借助Aspose.OCR ,使用 Python 提取JPG图像文本、将JPG图像转换为Word
  • 本地服务器端部署基于大模型的通用OCR项目——dots.ocr
  • 达梦数据库数据守护集群启动与关闭标准流程
  • docker安装FFmpeg