ConcurrentHashMap 超详细详解
ConcurrentHashMap 超详细详解一、基础定位ConcurrentHashMap是 Java 线程安全的哈希表替代老旧线程安全集合Hashtable同时弥补HashMap并发下死循环、数据丢失问题HashMap并发读写会链表循环、数据覆盖线程不安全Hashtable全表加synchronized锁粒度极大并发性能极差ConcurrentHashMap分段锁 / 分段 CASsynchronized锁粒度极小高并发读写性能优秀。版本区分核心JDK7分段锁 Segment底层数组 链表多段独立锁JDK8/JDK9/JDK17取消 SegmentCAS 节点 synchronized底层数组 链表 / 红黑树锁粒度降到哈希桶单个节点。二、JDK 7 实现原理过时仅做对比理解1. 底层结构plaintext[Segment[HashEntry链表], Segment[], ...]外层Segment[]分段数组默认 16 段每段独立锁内层每个 Segment 内部是HashEntryK,V[]哈希桶桶下挂单向链表Segment继承ReentrantLock自身就是锁对象。2. 并发机制写操作put/remove先定位 key 对应 Segment调用lock()独占锁操作完成释放只会锁住一个分段其他分段并发读写不受影响读操作get无锁HashEntry 的value、next用volatile修饰保证内存可见性读到最新值仅 value 为 null 时加锁重读扩容单段独立扩容互不干扰。缺陷默认分段 16并发上限固定分段越多内存占用越高链表查询效率低hash 冲突严重时 O (n)size () 统计总数需要锁住全部 Segment并发统计开销大。三、JDK 8 核心实现主流版本重点3.1 底层数据结构和 HashMap 结构统一数组 (Node []) 单向链表 红黑树哈希桶数组transient volatile NodeK,V[] tablevolatile 保证数组可见Node普通链表节点val、next均为volatileTreeNode红黑树节点冲突链表长度≥8 转红黑树≤6 退回链表ForwardingNode扩容标记节点table 扩容时旧桶挂载该节点线程碰到协助扩容ReservationNodecomputeIfAbsent 计算占位节点。3.2 并发核心机制CAS synchronized取消 Segment锁粒度从分段缩小到单个哈希桶头节点并发能力大幅提升无锁 CAS初始化 table、空桶插入新节点、扩容转移节点全部用 CAS 乐观锁无阻塞桶头 synchronized 锁哈希桶已有数据时锁住当前桶的首节点仅阻塞同一个 hash 桶的并发操作不同桶完全并发volatile 关键字table、节点 val/next、size 计数器保证多线程内存可见。CAS 关键方法java运行// 比较并替换数组指定下标节点成功返回true static final K,V boolean casTabAt(NodeK,V[] tab, int i, NodeK,V c, NodeK,V v) // 获取指定下标节点 static final K,V NodeK,V tabAt(NodeK,V[] tab, int i) // 设置指定下标节点 static final K,V void setTabAt(NodeK,V[] tab, int i, NodeK,V v)3.3 核心属性java运行// 哈希表数组初始化懒加载第一次put才创建 transient volatile NodeK,V[] table; // 扩容时临时转移数组非扩容时为null private transient volatile NodeK,V[] nextTable; // 计数器控制高16位记录size低16位存储扩容阈值/标记 private transient volatile long baseCount; // 扩容阈值容量*负载因子默认0.75 private transient int threshold; // 扩容控制标记负数扩容中正数扩容并行线程数 private transient volatile int sizeCtl;sizeCtl 状态说明0table 未初始化0初始化 / 扩容阈值-1正在初始化负数n-2-(1扩容线程数)代表多线程协同扩容。3.4 put () 完整流程核心源码逻辑java运行public V put(K key, V value) { return putVal(key, value, false); } final V putVal(K key, V value, boolean onlyIfAbsent) { // 不允许key/value为nullHashMap允许key一个null、多个null value if (key null || value null) throw new NullPointerException(); // 1. 计算hash扰动函数减少冲突 int hash spread(key.hashCode()); int binCount 0; for (NodeK,V[] tab table;;) { // 自旋循环直到插入成功 NodeK,V f; int n, i, fh; // 分支1table未初始化CAS初始化数组 if (tab null || (n tab.length) 0) tab initTable(); // 分支2当前桶为空CAS直接新建Node放入桶成功跳出循环 else if ((f tabAt(tab, i (n - 1) hash)) null) { if (casTabAt(tab, i, null, new NodeK,V(hash, key, value, null))) break; } // 分支3当前桶是ForwardingNode说明正在扩容当前线程协助扩容 else if ((fh f.hash) MOVED) tab helpTransfer(tab, f); // 分支4桶已有数据锁住桶头节点f单桶独占锁 else { V oldVal null; synchronized (f) { // 双重校验防止锁获取前桶头节点被替换 if (tabAt(tab, i) f) { // 链表节点 if (fh 0) { binCount 1; // 遍历链表key存在则覆盖不存在尾插 for (NodeK,V e f;; binCount) { K ek; if (e.hash hash ((ek e.key) key || (ek ! null key.equals(ek)))) { oldVal e.val; if (!onlyIfAbsent) e.val value; break; } NodeK,V pred e; if ((e e.next) null) { pred.next new NodeK,V(hash, key, value, null); break; } } } // 红黑树节点 else if (f instanceof TreeBin) { NodeK,V p; binCount 2; // 红黑树插入 if ((p ((TreeBinK,V)f).putTreeVal(hash, key, value)) ! null) { oldVal p.val; if (!onlyIfAbsent) p.val value; } } } } // 链表长度≥8转为红黑树 if (binCount ! 0) { if (binCount TREEIFY_THRESHOLD) treeifyBin(tab, i); if (oldVal ! null) return oldVal; break; } } } // 累加元素数量触发扩容判断 addCount(1L, binCount); return null; }关键特性禁止 null key /null valueHashMap 允许ConcurrentHashMap 直接抛 NPE避免并发下空值歧义懒加载第一次 put 才初始化 table节省内存空桶无锁 CAS 插入性能极高遇到扩容节点主动协助转移多线程并行扩容仅锁住单个桶头其他桶并发读写完全不受阻塞。3.5 get () 无锁读取流程读操作全程不加锁依靠volatile可见性保证数据实时计算 hash 定位桶下标获取桶头节点volatile 读取最新引用链表 / 红黑树遍历匹配 key直接返回 val若读到 ForwardingNode扩容中调用 find 协助读取转移中的数据。无锁设计让读操作几乎无性能损耗高并发读场景优势巨大。3.6 扩容机制transfer 多线程协同扩容触发条件addCount计数后元素数量 ≥ threshold容量 * 0.75扩容容量原容量 * 2多线程协同单线程扩容效率低其他线程 put/get 时发现ForwardingNode主动进入helpTransfer分担桶迁移任务每个线程分配一段哈希桶迁移迁移完成标记 ForwardingNode迁移逻辑每个桶内链表拆分为低位链表原下标、高位链表原下标 旧容量分别挂载到新 table扩容期间读写正常可用不会阻塞全局。3.7 size () 元素总数统计JDK7 需要锁住全部 Segment性能差 JDK8 采用分段计数器 baseCount CounterCell 数组类似 LongAdder低并发直接 CAS 修改baseCount高并发竞争线程分散写入不同CounterCellsize () baseCount 所有 CounterCell 累加和 无需加全局锁统计性能大幅提升。四、核心方法与并发特性对比1. 读写分离优势读无锁volatile 可见写仅锁单个哈希桶不同桶并行写入 远优于 Hashtable 全局锁。2. 常用方法并发语义get(key)无阻塞返回最新可见值put(k,v)覆盖旧值单桶互斥putIfAbsent(k,v)key 不存在才插入原子操作remove(k)/remove(k,v)原子删除锁桶头computeIfAbsent(k, func)不存在则执行函数计算 value 插入全程原子compute(k, func)存在则更新原子操作merge(k, val, func)合并更新原子 以上复合操作都是原子操作并发下不会出现数据竞争问题HashMap 无原子复合方法。五、和 HashMap、Hashtable 完整对比表格特性HashMapHashtableConcurrentHashMap(JDK8)线程安全不安全安全安全锁机制无锁方法全量 synchronizedCAS 桶节点 synchronizednull 键值允许 1 个 null key任意 null value不允许 null不允许 null key/value底层结构数组 链表 红黑树数组 链表数组 链表 红黑树并发性能高单线程极低并发极低全局锁极高细粒度锁扩容单线程单线程多线程协同扩容复合原子操作无有但全局锁丰富原子 API初始容量161116负载因子0.750.750.75六、常见面试高频考点为什么废弃 Segment 分段锁Segment 本质是 ReentrantLock内存开销大并发上限固定 16JDK8 锁粒度细化到单个桶并发度更高、内存占用更小。为什么不允许 null key/value并发场景下get 返回 null 无法区分key 不存在 /value 本身是 null无锁读无法同步判断容易产生业务逻辑歧义。扩容时其他线程读写是否阻塞不会全局阻塞读可正常访问旧 table写线程会协助扩容扩容完成切换新表全程可用。synchronized 锁桶头为什么安全同一 hash 桶的元素只会竞争同一个头节点锁不同桶互不干扰锁粒度极小插入前双重校验桶头引用防止 CAS 替换后锁失效。并发下 HashMap 死循环ConcurrentHashMap 为什么不会HashMap 扩容单线程头插法并发转移链表形成循环 ConcurrentHashMap 扩容多线程尾拆分链表且迁移时加桶锁不会出现环形链表。size () 为什么不用加全局锁使用 LongAdder 思想baseCountCounterCell 分散计数牺牲微小精度换取超高并发统计性能最终求和是近似值统计过程有新增删除无法做到精准瞬时总数。七、使用场景与注意事项使用场景多线程同时读写哈希表替代 Hashtable、Collections.synchronizedMap(new HashMap())高并发缓存、本地消息映射、多线程计数映射。注意事项复合操作优先使用内置原子方法putIfAbsent/computeIfAbsent不要if(get()null) put()非原子会并发覆盖 错误示例java运行// 线程不安全 if (map.get(key) null) { map.put(key, 1); } // 正确原子写法 map.computeIfAbsent(key, k - 1);迭代器是弱一致性不是快速失败fail-fast遍历期间其他线程新增 / 删除迭代器不会抛ConcurrentModificationException可能读到部分新数据HashMap 迭代器 fail-fast并发修改直接报错大批量数据读写可合理设置初始容量减少频繁扩容开销不要存储 null否则直接 NPE。