ConcurrentHashMap 深度解析:从 JDK7 到 JDK8 的演进与并发安全保障
在 Java 并发编程中ConcurrentHashMap是线程安全哈希表的核心实现相比Hashtable的全表锁机制它通过更细粒度的锁设计和数据结构优化实现了更高的并发性能。一、JDK7 的 ConcurrentHashMap 实现原理1.1 核心数据结构JDK7 中ConcurrentHashMap采用分段锁设计核心数据结构由三层组成Segment 数组ConcurrentHashMap的核心锁容器每个Segment继承自ReentrantLock独立承担锁功能默认并发级别为 16即 16 个Segment。HashEntry 数组每个Segment内部维护一个HashEntry数组作为哈希表的核心存储结构。链表HashEntry是链表节点包含key、hash、next均为final修饰保证不变性和valvolatile修饰保证可见性。1.2 初始化过程默认初始容量为 16负载因子为 0.75并发级别为 16。初始化时先计算Segment数组的长度大于等于并发级别的最小 2 的幂然后初始化第一个Segment其余Segment采用懒加载机制首次使用时初始化。1.3 put 操作流程计算key的哈希值定位到对应的Segment。调用lock()获取该Segment的独占锁。定位到Segment内HashEntry数组的索引遍历链表查找key。若找到key直接更新val若未找到创建新HashEntry插入链表头部。检查HashEntry数量是否超过阈值容量 × 负载因子若超过则扩容为原容量的 2 倍。调用unlock()释放锁。1.4 get 操作流程计算key的哈希值定位到Segment和HashEntry数组的索引。遍历链表查找key由于HashEntry的val和next均为volatile修饰保证了可见性因此无需加锁即可获取最新值。1.5 扩容机制扩容仅在单个Segment内部进行不影响其他Segment。扩容时创建新的HashEntry数组容量为原数组的 2 倍将原数组的所有节点重新哈希后迁移到新数组。二、JDK8 的 ConcurrentHashMap 实现原理JDK8 对ConcurrentHashMap进行了彻底重构取消了分段锁设计改用Node 数组 CAS Synchronized实现并引入红黑树优化链表过长的问题。2.1 核心数据结构Node 数组核心存储结构替代了 JDK7 的Segment数组。Node是链表节点val和next均为volatile修饰。链表/红黑树当链表长度 ≥ 8 且数组长度 ≥ 64 时链表转换为红黑树TreeNode当红黑树节点数 ≤ 6 时转换回链表。TreeBin用于封装TreeNode作为红黑树的根节点容器。ForwardingNode扩容时用于标记旧数组的节点其他线程看到该节点会协助扩容。2.2 初始化过程默认初始容量为 16负载因子为 0.75。Node数组采用懒加载机制首次执行put操作时才初始化通过 CAS 控制初始化的并发安全。2.3 put 操作流程检查Node数组是否初始化若未初始化则通过 CAS 初始化。计算key的哈希值定位到Node数组的索引。若该索引位置为null通过 CAS 直接插入新Node。若该索引位置为ForwardingNode说明数组正在扩容当前线程协助扩容。否则用synchronized锁住该索引位置的头节点遍历链表或红黑树查找key。若找到key更新val若未找到插入链表尾部或红黑树。检查链表长度是否 ≥ 8若满足则判断是否需要树化数组长度 ≥ 64 则树化否则扩容。释放锁检查是否需要扩容元素数量达到阈值则扩容。2.4 get 操作流程计算key的哈希值定位到Node数组的索引。检查头节点是否为目标key若是则直接返回val。若头节点是TreeBin则通过红黑树的find方法查找否则遍历链表查找。由于Node的val和next为volatile修饰保证了可见性因此无需加锁。2.5 树化与反树化树化条件链表长度 ≥ 8 且Node数组长度 ≥ 64。若数组长度 64则先扩容而非树化。反树化条件红黑树节点数 ≤ 6 时转换回链表。2.6 扩容机制当Node数组的元素数量达到阈值容量 × 负载因子时触发扩容创建新的Node数组容量为原数组的 2 倍。在旧数组的每个索引位置插入ForwardingNode标记该位置正在迁移。多个线程可并发参与扩容每个线程处理旧数组的一部分区间步长由 CPU 核心数决定通过 CAS 控制并发安全。迁移完成后将ConcurrentHashMap的table引用指向新数组。三、JDK7 与 JDK8 的核心差异对比维度JDK7JDK8锁粒度Segment级别默认 16 个锁Node级别锁住数组索引的头节点数据结构Segment数组 HashEntry数组 链表Node数组 链表 红黑树锁实现ReentrantLockCASsynchronized扩容方式单个Segment独立扩容整个Node数组并发扩容查找效率链表遍历O(n)链表遍历O(n)或红黑树查找O(logn)并发性能受限于Segment数量高并发下性能瓶颈明显锁粒度更细红黑树优化查找并发扩容性能显著提升四、并发安全的保证机制4.1 JDK7 的并发安全保证分段锁Segment继承ReentrantLockput、remove等修改操作需获取锁保证原子性。volatile 可见性HashEntry的val和next为volatile修饰get操作无需加锁即可读取最新值。不变性HashEntry的key、hash、next为final修饰防止并发修改导致的结构破坏。4.2 JDK8 的并发安全保证volatile 可见性Node的val和next为volatile修饰保证get操作的可见性。CAS 原子性插入头节点、初始化数组、扩容时的并发控制均通过 CAS 实现保证原子性。synchronized 锁锁住数组索引的头节点保证链表或红黑树修改操作的原子性。并发扩容通过ForwardingNode标记和多线程协作实现高效的并发扩容。TreeBin 状态控制TreeBin内部通过volatile状态变量和 CAS 控制红黑树的访问保证并发安全。五、代码示例5.1 使用示例package com.jam.demo; import com.google.common.collect.Maps; import lombok.extern.slf4j.Slf4j; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; /** * ConcurrentHashMap 使用示例 * * author ken */ Slf4j public class ConcurrentHashMapDemo { public static void main(String[] args) { MapString, String concurrentHashMap new ConcurrentHashMap(); MapString, String hashMap Maps.newHashMap(); concurrentHashMap.put(key1, value1); concurrentHashMap.put(key2, value2); log.info(获取 key1: {}, concurrentHashMap.get(key1)); log.info(是否包含 key2: {}, concurrentHashMap.containsKey(key2)); concurrentHashMap.remove(key1); log.info(移除 key1 后大小: {}, concurrentHashMap.size()); concurrentHashMap.replace(key2, newValue2); log.info(替换 key2 后的值: {}, concurrentHashMap.get(key2)); } }5.2 并发安全测试示例package com.jam.demo; import lombok.extern.slf4j.Slf4j; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; /** * ConcurrentHashMap 并发安全测试示例 * * author ken */ Slf4j public class ConcurrentHashMapConcurrencyTest { private static final int THREAD_COUNT 100; private static final int OPERATIONS_PER_THREAD 1000; public static void main(String[] args) throws InterruptedException { MapString, Integer concurrentHashMap new ConcurrentHashMap(); ExecutorService executorService Executors.newFixedThreadPool(THREAD_COUNT); CountDownLatch countDownLatch new CountDownLatch(THREAD_COUNT); long startTime System.currentTimeMillis(); for (int i 0; i THREAD_COUNT; i) { final int threadId i; executorService.submit(() - { try { for (int j 0; j OPERATIONS_PER_THREAD; j) { String key key- threadId - j; concurrentHashMap.put(key, j); concurrentHashMap.get(key); } } finally { countDownLatch.countDown(); } }); } countDownLatch.await(); executorService.shutdown(); long endTime System.currentTimeMillis(); log.info(总操作数: {}, THREAD_COUNT * OPERATIONS_PER_THREAD * 2); log.info(最终 Map 大小: {}, concurrentHashMap.size()); log.info(耗时: {} ms, endTime - startTime); } }六、总结从 JDK7 到 JDK8ConcurrentHashMap通过数据结构和锁机制的重构实现了性能的显著提升。JDK7 的分段锁设计是早期并发优化的经典思路而 JDK8 则通过细粒度锁、红黑树和并发扩容等技术进一步释放了并发编程的潜力。需要注意的是ConcurrentHashMap只能保证单个操作的线程安全对于复合操作如先检查后执行仍需通过额外的同步机制来保证原子性。