ConcurrentHashMap1.7与1.8的区别

23 阅读2分钟

Java ConcurrentHashMapJDK 1.7 vs 1.8 的核心区别:并发控制从“分段锁 Segment”变成“桶级 CAS + 少量 synchronized + 红黑树” ,结构更简单、吞吐更高、内存更省。


1) 数据结构差异

JDK 1.7:Segment + HashEntry(分段)

  • ConcurrentHashMap 内部是 Segment[]
  • 每个 Segment 里维护一个 HashEntry[](类似 HashMap 桶数组)
  • Segment 继承 ReentrantLock:并发靠“锁住某个 Segment”

并发粒度:Segment 级别(默认 16 个左右,最多同时 16 把锁并发写)

JDK 1.8:Node[] + CAS + TreeBin(桶级)

  • 直接是 Node[] table
  • 桶内是链表 Node,冲突严重时树化成 红黑树 TreeBin
  • 并发写优先用 CAS,必要时对桶头用 synchronized(锁粒度到桶/链表/树)

并发粒度:桶级别(理论上更细)


2) 锁机制差异(最重要)

1.7:ReentrantLock(Segment 锁)

  • 写操作:锁住对应 Segment
  • 读操作:多数无锁(依赖 volatile 保证可见性)

优点:实现清晰
缺点:锁粒度不够细,热点段会成为瓶颈;Segment 结构额外占内存

1.8:CAS + synchronized(桶锁)

  • put 时:

    • 桶为空:CAS 直接放入
    • 桶不空:synchronized (桶头节点) 进入桶级加锁进行链表/树操作
  • 使用 synchronized 的原因:JDK 8 对 synchronized 做了大量优化(偏向锁/轻量锁等)

优点:热点更分散,吞吐更好;结构更省内存
缺点:实现复杂度更高


3) 红黑树优化(1.8 新增)

  • 1.7:桶冲突一直是链表,极端情况下退化 O(n)
  • 1.8:链表长度超过阈值会 树化,查询接近 O(log n)

常见规则(记住大概即可):

  • 链表长度到 8 触发树化意向
  • table 容量至少 64 才真正树化(否则优先扩容)

4) 扩容机制差异

1.7:Segment 内各自扩容

  • 扩容发生在 Segment 维度,锁住 Segment 后 rehash
  • 扩容影响范围较小,但总体结构重

1.8:全表扩容 + 多线程协助迁移

  • 引入 sizeCtltransferIndex 等控制字段
  • 扩容时多个线程可以“帮忙搬迁桶”(协同 transfer),降低单线程扩容抖动
  • 桶迁移后会放一个 ForwardingNode 标记“已迁移”

5) 计数方式差异(size 统计)

1.7:

  • 维护 Segment 级 count
  • size() 需要汇总多个 Segment,可能重试(弱一致)

1.8:

  • 使用 baseCount + CounterCell(类似 LongAdder 分散热点)
  • 高并发下计数冲突更小,性能更稳

6) 小结对比表

维度JDK 1.7JDK 1.8
结构Segment[] + HashEntry[]Node[] + 链表/红黑树
锁粒度Segment 锁(ReentrantLock)桶级 CAS + synchronized
基本无锁基本无锁
进入 Segment 锁空桶 CAS;非空桶 synchronized
冲突优化链表链表→红黑树
扩容Segment 内扩容全表扩容,多线程协助迁移
计数Segment 汇总baseCount + CounterCells