ConcurrentHashMap分段锁如何工作

wen java案例 3

本文目录导读:

ConcurrentHashMap分段锁如何工作

  1. Java 7 中的分段锁(Segment)
  2. 分段锁的优势
  3. Java 8 的改进:放弃分段锁
  4. 总结与对比

ConcurrentHashMap 的分段锁机制是 Java 7 及之前版本中的核心设计(Java 8 之后改用 CAS + synchronized + 红黑树 来优化锁粒度),分段锁的核心思想是:将数据分片,分别加锁,从而提高并发效率。

下面先详细解释 Java 7 的分段锁原理,再说明 Java 8 的改进。


Java 7 中的分段锁(Segment)

数据结构

  • 整个 ConcurrentHashMap 由一个 Segment 数组 构成。
  • 每个 Segment 本身就是一个 小型 HashMap(内部包含一个 HashEntry 数组)。
  • 每个 Segment 继承自 ReentrantLock(可重入锁),即每个 Segment 拥有自己的锁。

示意图:

ConcurrentHashMap
├── Segment[0]   (锁1)
│   ├── HashEntry[0]
│   ├── HashEntry[1]
│   └── ...
├── Segment[1]   (锁2)
│   ├── HashEntry[0]
│   └── ...
├── ...
└── Segment[15]  (锁16)   // 默认16个Segment

锁的粒度

  • 默认并发级别:16(即 16 个 Segment)。
  • 锁的粒度:一个锁只保护一个 Segment 内的所有 HashEntry。
  • 不同的 Segment 之间 完全独立,可以同时被不同线程访问。

读写操作流程

put 操作

// 简化逻辑
public V put(K key, V value) {
    int hash = hash(key);
    int segmentIndex = (hash >>> segmentShift) & segmentMask;
    Segment<K,V> s = segments[segmentIndex];
    // 对单个 Segment 加锁(可重入锁)
    s.lock();
    try {
        // 在 Segment 内部的 HashEntry 数组中插入或更新
        // 每个 Segment 内部与 HashMap 类似
    } finally {
        s.unlock();
    }
}
  • 只锁一个 Segment,其他 15 个 Segment 完全不受影响,可被其他线程并发操作。

get 操作

public V get(Object key) {
    int hash = hash(key);
    int segmentIndex = (hash >>> segmentShift) & segmentMask;
    Segment<K,V> s = segments[segmentIndex];
    // get 操作不加锁,依靠 volatile 关键字保证可见性
    // HashEntry 的 value 字段是 volatile 的
    HashEntry<K,V> e = s.getEntry(hash, key);
    return e == null ? null : e.value;
}
  • get 不加锁,因此读取效率极高。
  • 通过 volatile 保证:读线程一定能看到其他线程写入的最新值。

size 操作

  • 需要获取所有 Segment 的大小之和。
  • 不加锁的尝试:先不加锁累加两次。
  • 如果两次结果一致,则直接返回。
  • 如果不一致,则 锁住所有 Segment 再计算。

分段锁的优势

特性 普通 HashMap(线程不安全) Hashtable / Collections.synchronizedMap ConcurrentHashMap(分段锁)
锁粒度 整张表一把锁 每个 Segment 一把锁
并发读写 线程不安全 完全串行,效率低 不同 Segment 可并发
读操作 无锁 加锁 无锁(volatile)

核心优势:在多线程环境下,多个线程可以同时写入不同的 Segment,极大提升吞吐量。


Java 8 的改进:放弃分段锁

Java 8 对 ConcurrentHashMap 进行了重构,不再使用 Segment,而是改为:

新的数据结构

  • 内部直接使用 Node 数组 + 链表 / 红黑树(与 HashMap 类似)。
  • 使用 synchronized + CAS 实现并发控制。

锁粒度更细

  • 锁的粒度:从“一个 Segment” 降为 “一个桶(bucket)”(即数组的一个位置)。
  • 当多个线程操作不同的哈希桶时,可以完全并发,无需竞争同一把锁。

实现方式

  • put 操作
    • 如果桶为空:使用 CAS 无锁插入。
    • 如果桶不为空:对该桶的 头节点加 synchronized 锁,然后遍历链表/红黑树。
  • get 操作:完全无锁,通过 volatile 保证可见性。
  • 扩容:支持 多线程协助扩容(transfer),而不是像 Java 7 那样锁住整个 Segment。

为什么比分段锁更好?

  • 锁粒度更细:分段锁最多 16 个并发写入(默认),Java 8 理论上可以支持所有桶同时写入(取决于哈希分布)。
  • 内存占用更小:不需要维护 Segment 数组和额外的锁对象。
  • 查询效率更高:红黑树将最坏情况从 O(n) 降为 O(log n)。

总结与对比

版本 锁机制 锁粒度 并发写入上限 读操作锁
Java 7 Segment 继承 ReentrantLock 每个 Segment 默认 16 无锁(volatile)
Java 8 CAS + synchronized 每个哈希桶 理论所有桶 无锁(volatile)
  • Java 7 分段锁:把 Map 分成若干段,每段一把锁,不同段可并发。
  • Java 8 改进:去掉分段,直接用 CAS 和桶级锁(synchronized),粒度更细、性能更好、内存更低。

抱歉,评论功能暂时关闭!