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

HashMap 和 ConcurrentHashMap的区别

一、HashMap:非线程安全的哈希表实现

1. 核心数据结构
// 简化版源码(Java 8+)
transient Node<K,V>[] table;  // 桶数组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 Node<K,V> {TreeNode<K,V> parent;  // 红黑树结构TreeNode<K,V> left;TreeNode<K,V> right;boolean red;
}
  • 数组 + 链表/红黑树:默认容量 16,负载因子 0.75

  • 树化条件:链表长度 ≥8 且桶数组长度 ≥64

  • 退化条件:树节点数 ≤6

2. 关键操作原理
扩容机制(Java 8 优化)
  • 新位置 = 原位置 OR 原位置 + 旧容量

  • 通过 (e.hash & oldCap) == 0 判断位置:

    if ((e.hash & oldCap) == 0) {newTab[j] = loHead;     // 保持原索引
    } else {newTab[j + oldCap] = hiHead;  // 新索引 = 原索引 + 旧容量
    }

3. 线程安全问题
问题类型原因说明
数据覆盖多线程同时 put 时覆盖值(非原子操作)
死循环Java 7 头插法扩容导致链表成环(Java 8 尾插法解决)
size 不准确多线程并发修改 size 计数器
迭代器 fail-fast并发修改触发 ConcurrentModificationException

二、ConcurrentHashMap:线程安全的高并发实现

1. 演进历史
  • Java 7:分段锁(Segment),默认 16 段

  • Java 8+:CAS + synchronized + volatile(桶级别锁)

2. Java 8 底层实现
(1) 核心数据结构
transient volatile Node<K,V>[] table;  // volatile 保证可见性static class Node<K,V> implements Map.Entry<K,V> {volatile V val;          // volatile 修饰值volatile Node<K,V> next; // volatile 修饰指针
}
  • 禁止 null 键值:避免并发歧义

  • 节点锁:synchronized 锁定单个桶的首节点

(2) 线程安全机制
机制应用场景实现方式
CAS初始化数组、插入空桶、计数更新U.compareAndSwapObject()
synchronized锁定非空桶synchronized (f) { ... }(f 是桶的首节点)
volatile保证内存可见性Node 的 val/next 字段声明为 volatile
ForwardingNode扩容时安全访问特殊节点标记正在迁移的桶

3. 高并发优化设计
  1. 多线程协同扩容

    • 线程 put 时发现桶为 ForwardingNode,会协助迁移数据

    • 通过 transferIndex 分配迁移任务区间

  2. 高效计数机制

    private transient volatile CounterCell[] counterCells;
    • 类似 LongAdder 的分段计数,减少 CAS 竞争

  3. 查询免锁

    • 依赖 volatile 和 Unsafe.getObjectVolatile 保证可见性

    • 遍历过程无需加锁


三、HashMap vs ConcurrentHashMap 对比

特性HashMapConcurrentHashMap (Java 8)
线程安全❌ 非线程安全✅ 桶级别锁 + CAS
锁粒度无锁单个桶首节点 (synchronized)
Null 支持✅ 允许 null 键/值❌ 禁止 null(避免并发歧义)
迭代器Fail-Fast弱一致性迭代器
扩容机制单线程扩容多线程协同扩容
计数实现size 变量CounterCell 分段计数
树化阈值链表长度 ≥8同 HashMap
内存可见性无特殊保障volatile + Unsafe 操作
死锁风险无锁无死锁桶级别锁,无死锁风险
适用场景单线程/只读多线程高并发读写场景

四、关键问题解析

1. 为什么 ConcurrentHashMap 不允许 null?
  • 并发歧义问题:无法区分 "key 不存在" 还是 "key 对应的值为 null"

  • 设计一致性:Map.get(key) 返回 null 时,在并发环境下无法确定真实含义

2. 为什么 Java 8 抛弃分段锁?
  • 内存开销:Segment 数组消耗额外内存

  • 伪共享问题:CPU 缓存行失效影响性能

  • 锁粒度粗:同 Segment 不同桶仍会竞争锁

  • 桶级别锁优势

    • 碰撞概率低时几乎无锁竞争

    • synchronized 随 JVM 优化性能提升

3. 如何保证 size() 的准确性?
  • 分治计数

    final long sumCount() {CounterCell[] cs = counterCells;long sum = baseCount;if (cs != null) {for (CounterCell c : cs)sum += c.value;}return sum;
    }
    • 更新时优先尝试 CAS 修改 baseCount

    • 竞争激烈时分散到 CounterCell[] 中

    • 最终结果为所有分片求和


五、最佳实践建议

  1. 选型指南

    • 单线程/只读场景 → HashMap

    • 高并发读写 → ConcurrentHashMap

    • 强一致性需求 → Collections.synchronizedMap()

  2. 性能优化

    • 预分配容量:避免扩容开销

    • 避免可变对象作为 Key:防止哈希值变化

    • 复杂对象:重写 hashCode() 和 equals()

  3. 并发使用注意

    • 迭代器弱一致性:不保证实时最新数据

    • 批量操作:使用 forEach/search/reduce 并行方法

示例场景:实现高并发缓存

ConcurrentHashMap<String, AtomicLong> cache = new ConcurrentHashMap<>();// 原子性累加
cache.computeIfAbsent("counter", k -> new AtomicLong()).incrementAndGet();

http://www.lqws.cn/news/562519.html

相关文章:

  • 数据结构之——顺序栈与链式栈
  • 【图像处理基石】什么是摄影的数码味?
  • Redis—主从复制
  • 跟着AI学习C#之项目实战-电商平台 Day5
  • pandas 优雅处理值类型为list的列的csv读写问题
  • Day45 Tensorboard使用介绍
  • 《垒球百科》垒球有多重·垒球1号位
  • 在 RT-Thread 中实现 Shell 控制台的设计与源码剖析
  • C++入门(笔记)
  • MySQL 索引 -- 磁盘,主键索引,唯一索引,普通索引,全文索引
  • AC自动机 多模式字符串匹配(简单版)
  • 马斯克的 Neuralink:当意念突破肉体的边界,未来已来
  • 嵌入式原理与应用篇---ARM
  • 深度学习量化数值类型
  • 机器学习——线性回归
  • 数据结构与算法学习笔记(Acwing 提高课)----动态规划·单调队列优化DP
  • Requests源码分析:底层逻辑
  • 模板方法 + 策略接口
  • glog使用详解和基本使用示例
  • 数据结构:顺序表
  • Lua现学现卖
  • Java代码阅读题
  • 06-three.js 创建自己的缓冲几何体
  • 某音Web端消息体ProtoBuf结构解析
  • 【网络安全】网络安全中的离散数学
  • 机器学习算法-K近邻算法-KNN
  • BUUCTF [ACTF新生赛2020]music 1
  • SpringMVC系列(五)(响应实验以及Restful架构风格(上))
  • 【学习】《算法图解》第七章学习笔记:树
  • [论文阅读] 软件工程 | 微前端在电商领域的实践:一项案例研究的深度解析