ConcurrentHashMap 的分段锁被废弃是因为 Segment 在多数场景下成为性能拖累;Java 8 改用 CAS + synchronized,实现更细粒度锁、无全局竞争、更低内存开销,并支持并发扩容。

ConcurrentHashMap 的分段锁为什么被废弃了
Java 8 彻底移除了 Segment 数组,不是因为写得不好,而是它在多数场景下成了性能拖累。CAS + synchronized 替代后,单个桶的锁粒度更细、无全局 segment 竞争、内存开销更低。老版用 rehash 时要锁整个 Segment,而新版本只锁链表头或红黑树根节点,扩容也能并发推进。
常见错误现象:ConcurrentHashMap 在 Java 7 中调用 size() 会遍历所有 Segment 并加锁求和,高并发下卡顿明显;Java 8 改成用 baseCount + CounterCell[],但 size() 仍是弱一致性结果——这点很多人误以为“变准了”,其实只是更快了,依然不保证实时。
LongAdder 的 Cell 数组本质是分段计数,不是分段锁
LongAdder 没有锁,它的 Cell[] 是典型的“线程本地槽位 + 竞争退避”设计:每个线程优先往自己绑定的 Cell 写,冲突时才尝试换槽或 fallback 到 baseCount。这和传统分段锁的“预先划分资源区间+加锁访问”逻辑完全不同。
使用场景:
立即学习“Java免费学习笔记(深入)”;
- 高并发累加(如请求计数、指标统计),
AtomicLong在多核下 CAS 失败率飙升,LongAdder能压低 5–10 倍争用开销 - 不要用于需要强一致读写的场景,比如作为循环终止条件或状态判断依据
-
sum()成本随Cell数量线性增长,如果线程数长期稳定且不多,AtomicLong反而更轻量
还有哪些类在用类似分段思想但没叫“分段锁”
真正延续分段思路(预划分 + 局部控制)的,现在基本都转向了无锁或更轻量同步机制,但仍有几个典型:
-
ForkJoinPool的工作窃取队列:每个线程维护自己的双端队列(WorkQueue),任务提交优先入本地队列,窃取时才跨队列操作——这是运行时分段,不是锁层面的 -
ThreadLocalRandom的种子隔离:每个线程持有独立seed和probe,避免Random全局竞争,底层用的是类似Cell的数组索引定位 -
StampedLock的乐观读不涉及分段,但它允许读写分离,间接缓解了“全表锁”压力,在某些缓存场景中替代了过去用分段锁保护大 Map 的做法
注意:Phaser 虽然支持分阶段等待,但同步点是集中管理的,没有数据分片;Striped64(LongAdder 的父类)才是分段计数的通用骨架,但 JDK 没暴露出来供直接复用。
自己实现分段结构时最容易踩的坑
别照着 Java 7 的 Segment 抄——那个模型在现代 JVM 上容易因 false sharing、GC 压力、分支预测失败反而变慢。
- 分段数量别硬编码为 16 或 32,应基于预期并发线程数动态计算,常用
Runtime.getRuntime().availableProcessors() * 2作起点 - 每个段内别再套一层锁:比如用
synchronized(segment)包裹HashMap操作,不如直接用ConcurrentHashMap;真要自定义,优先考虑ReentrantLock配合 tryLock 避免死锁 -
Cell[]类型数组必须用@Contended(JDK 8+)或手动填充字段防 false sharing,否则多个Cell落同一缓存行,性能比单点还差 - 扩容时别让所有段同时 rehash,要像
ConcurrentHashMap那样逐段迁移,否则瞬间锁住全部段,退化成全局锁
分段从来不是银弹,它把一个问题拆成多个小问题,但每个小问题仍要自己处理好可见性、有序性和回收——尤其是段内对象生命周期管理,稍不注意就泄漏或错乱。










