jdk 1.7 通过 segment 分段锁实现线程安全:默认16个segment,读操作无锁,写操作仅锁定对应segment,采用两次hash定位,支持并发度调整但需为2的幂;size()非强一致。

ConcurrentHashMap 在 JDK 1.7 里怎么靠 Segment 分段锁实现线程安全
JDK 1.7 的 ConcurrentHashMap 不是给整个 map 加一把大锁,而是把数据拆成 16 个(默认)独立的 Segment,每个 Segment 是一个带锁的小型哈希表。读操作完全无锁,写操作只锁住对应的那个 Segment,其他段照常工作。
实际执行 put() 时会走两次 hash:第一次用高位 hash 定位到哪个 Segment,第二次用低位 hash 定位到该 Segment 内部数组的具体位置。这叫“锁分离”——不是锁数据,是锁数据所在的逻辑分区。
- 默认并发度是 16,可通过构造函数传
concurrencyLevel调整,但最终会被提升为最接近的 2 的幂次(如传 12 → 实际用 16) - 如果所有写操作都挤在同一个
Segment上(比如 key 的高位 hash 总是一样),那并发度就白设了,性能退化成单线程 -
size()方法要遍历全部Segment并加锁求和,可能阻塞、也可能返回估算值(尝试三次不一致就直接返回估算),不是强一致性
JDK 1.8 彻底去掉 Segment,改用 CAS + synchronized 锁桶节点
JDK 1.8 把结构简化回类似 HashMap 的 Node[] 数组,但每个数组槽位(bin)的头节点自己就是锁对象。插入/更新时先用 cas 尝试无锁写入;失败则对那个 bin 的头节点加 synchronized,只锁这一小片区域。
链表长度 ≥ 8 且数组长度 ≥ 64 时,链表转为红黑树——这是为了防止极端 hash 碰撞导致查找退化成 O(n)。树化后锁的粒度仍是单个 bin,不是整棵树。
-
put()和get()都不再需要两次 hash,一次定位到底,性能开销更低 - 扩容是并发进行的:多个线程可协作搬运不同区段的节点,新老数组可同时服务读请求(通过
ForwardingNode中转) -
synchronized锁的是Node对象,不是类或 this,避免锁升级带来的开销;但若多个线程反复竞争同一 bin,仍会触发偏向锁撤销甚至重量级锁
为什么不能简单认为 “1.8 一定比 1.7 快”
快慢取决于场景。高冲突低并发时,1.7 的分段锁可能因 segment 数固定、负载不均反而更卡;而 1.8 的细粒度锁在热点 key 场景下容易引发锁争用,甚至因频繁锁升级拖慢吞吐。
- 1.7 的
Segment是继承ReentrantLock的,有公平/非公平模式、可中断等待等开销;1.8 的synchronized更轻量,但不可中断、不支持条件队列 - 1.7 的迭代器是弱一致性(不抛
ConcurrentModificationException),但可能漏掉刚插入的元素;1.8 迭代过程同样不阻塞写入,但结构变化(如树化、扩容)可能导致遍历跳过部分节点 - 如果你的应用还依赖
segments.length或segment.getXXX()这类 1.7 特有 API,升级到 1.8 会直接编译失败——它们全被删了
实际开发中该关注哪几个关键行为差异
别只盯着“线程安全”,更要盯住行为语义是否符合你业务预期。比如初始化、扩容策略、null 处理、以及某些方法在并发下的可见性边界。
-
computeIfAbsent()在 1.8 是原子的,但在 1.7 不可用(得手写双重检查 +putIfAbsent()) - 1.7 允许 key/value 为
null(虽然不推荐),1.8 明确禁止,put(null, v)直接抛NullPointerException - 1.8 的
mappingCount()返回long,解决 21 亿上限问题;1.7 的size()是int,超了就溢出 - 无论哪个版本,都不保证遍历顺序;也不保证
keySet().iterator()与values().iterator()的遍历进度同步——别拿两个迭代器做交叉比对










