Java 中 ConcurrentHashMap 1.7 和 1.8 之间有哪些区别?
- 标签:Java 集合、Java
🏛 业务架构师
这两代的区别本质是锁粒度的演进,背后反映的是业务并发规模的增长。
- 1.7 分段锁(Segment):把整个表拆成 16 个 Segment,每个 Segment 继承 ReentrantLock,锁住一段数据。并发度被段数写死,定位要先算段再算桶,两层 hash,内存也重。
- 1.8 CAS + synchronized:放弃分段,改为对每个桶的头节点加锁,锁粒度从"段"细化到"桶";空桶插入用 CAS 无锁竞争,冲突了才 synchronized 桶头,并且同样引入链表 + 红黑树。
从业务视角看这次演进:并发度从固定 16 变成"一个桶一把锁",等于随数据量自动伸缩的并发能力,高并发热点读场景(比如商品详情页的本地热点缓存、实时计数聚合)吞吐显著提升,同时内存占用下降。
业务选型上我的原则:多线程共享的可变 KV 容器一律 ConcurrentHashMap,不要心存侥幸用 Collections.synchronizedList 或 HashMap 加锁包装——前者性能差,后者容易漏包。这个决定的成本几乎为零,但能消掉一整类线上事故。
🏗 系统架构师
这题我按"锁粒度演进"来答:1.7 是 Segment 分段锁(默认 16 段,继承 ReentrantLock),并发度被段数锁死;1.8 抛弃分段,改为 CAS + synchronized 锁单个桶头节点,锁粒度从"段"降到"桶",并发度理论上等于数组长度。
其他关键差异:
- 数据结构:1.7 数组 + 链表;1.8 数组 + 链表 + 红黑树,退化阈值 6、树化阈值 8;
- 1.8 的 value、next 用 volatile 修饰,配合 Unsafe 的 tabAt/casTabAt 保证可见性与原子操作,get 无锁;
- 扩容上 1.8 支持多线程协助迁移(helpTransfer),写线程发现正在扩容会一起搬数据。
从架构角度这是典型的"用更细粒度锁换吞吐",我在设计高并发容器选型时也遵循同样思路:锁的边界越小,水平扩展能力越强,但实现复杂度和内存语义成本要评估清楚。
💻 全栈开发专家
一句话:1.7 是分段锁(Segment 继承 ReentrantLock,默认 16 段),1.8 抛弃分段,改成 CAS + synchronized 锁单个桶头节点。
主要区别:
- 数据结构:1.7 是 Segment 数组 + HashEntry 链表(两层 hash);1.8 是 Node 数组 + 链表/红黑树,和 HashMap 对齐。
- 锁粒度:1.7 锁一段(含多个桶);1.8 只锁一个桶头,粒度细得多,冲突概率低时只有一次 CAS,几乎无锁。
- size 统计:1.7 遍历各 Segment 的 count;1.8 用 baseCount + CounterCell 分散计数(类似 LongAdder),
size()是近似值。 - 扩容:1.8 支持多线程协助扩容(transfer 时发现 ForwardingNode 就帮忙迁移)。
工程细节:1.8 的 size() 不精确,做限流计数别依赖它,我会用 mappingCount() 或者外面的 AtomicLong。另外 get 全程无锁(val 用 volatile 保证可见性),读多写少场景性能非常好。