ConcurrentHashMap是HashMap的线程安全版本,核心目标是在高并发下提供安全的读写,同时尽量避免阻塞。它的实现经历了两个关键阶段:JDK 1.7 用“分段锁”降低锁粒度,JDK 1.8 改为“CAS + synchronized 锁单个桶”,并发度更高。
一、在 JDK1.7 中的实现原理
实现原理采用分段锁(Segment)机制。将数据分为多个 Segment(默认16个),每个 Segment 继承 ReentranLock,独立加锁。不同 Segment 可并发访问,提高了并发度。每个 Segment 内部维护一个 HashEntry 数组。
在 jdk1.7 中, ConcurrentHashMap 的结构如下:
HashEntry是ConcurrentHashMap的内部静态类,核心字段大致是:
static final class HashEntry<K,V> { final int hash; // key 的哈希值(final,不可变) final K key; // 键(final) volatile V value; // 值(volatile,保证可见性) volatile HashEntry<K,V> next; // 链表下一个节点(volatile) }一句话总结:ConcurrentHashMap是一个 Segment 数组;每个 Segment 继承 ReentrantLock 当锁用,内部又持有一个HashEntry[]数组;HashEntry是数组里的节点,用链表串起来真正存 key-value。
二、在 JDK1.8 中的实现原理
在 JDK1.8 中,实现原理废弃了分段锁,改用 CAS + synchronized。底层数据结构与 HashMap 一致(数组+链表+红黑树)。锁粒度细化到每个数据元素(桶),使用 synchronized 对链表头节点或树根节点加锁,同时利用 CAS 进行无锁操作。并发度更高,且引入红黑树优化链表查询性能。
桶为空时,CAS 插入:如果计算出的桶位置是空的,直接用
casTabAt(CAS 操作)把新节点放进去,不需要加锁。桶非空时,锁住头节点:如果桶里已经有元素,就用
synchronized锁住当前桶的头节点(f)。只要 hash 不冲突,线程操作的是不同的桶,就不会产生锁竞争。读操作基本无锁:
get方法通常不加锁,依靠Node中val和next的volatile语义来保证可见性。
JDK1.8 用锁单个桶头节点替代了锁整个段,并用CAS处理空桶插入,把锁的冲突概率降到了最低。
什么时候从链表变成红黑树?什么时候从红黑树退化为链表?
- 当链表长度 >= 8 且数组长度 >= 64 时,链表转为红黑树
- 当红黑树节点 <= 6 时,退化为链表。
三、补充知识
1、ReentranLock
ReentrantLock是 Java 在java.util.concurrent.locks包下提供的一个可重入的互斥锁,从 JDK 1.5 引入,用来替代synchronized的一部分场景。名字拆开就是两个关键词:Reentrant(可重入)+ Lock(锁)。
可重入是什么意思:
同一个线程可以多次获取同一把锁,不会把自己锁死。内部维护一个计数器:线程第一次lock()计数变 1,再次lock()变 2;每unlock()一次减 1,减到 0 才真正释放锁。
和 synchronized 的区别
| 维度 | synchronized | ReentrantLock |
|---|---|---|
| 层级 | JVM 关键字 | JDK 类(API 层) |
| 加锁/释放 | 自动(代码块结束自动释放) | 手动lock()/unlock(),必须finally释放 |
| 可重入 | 是 | 是 |
| 公平锁 | 不支持 | 支持(new ReentrantLock(true)) |
| 尝试加锁 | 不支持 | 支持tryLock()(可带超时,不阻塞或限时等待) |
| 可中断 | 不支持(等锁时不可中断) | 支持lockInterruptibly() |
| 条件变量 | 一个(wait/notify) | 多个Condition(newCondition()) |
| 锁状态查询 | 不能 | 能(isLocked()、getQueueLength()等) |
为什么 ConcurrentHashMap 1.7 的 Segment 要继承它
static final class Segment<K,V> extends ReentrantLock implements SerializableSegment 需要“自己就是一把锁”的能力,而ReentrantLock正好提供:
lock()/unlock()加解锁;可重入——同一线程在 Segment 内部操作时可能重复加锁,可重入避免死锁;
比
synchronized更灵活(比如可尝试加锁、可查询状态)。
所以ConcurrentHashMap1.7 里,每个 Segmentextends ReentrantLock,直接获得锁的全部能力,锁对象就是 Segment 自己,加锁就是segment.lock()。
2、CAS
CAS(Compare-And-Swap,比较并交换)是一条CPU 指令级的原子操作,不是 Java 发明的。它做三件事,且这三件事整体原子、不可被打断:
期待值是调用方自己给。典型模式是“读 → 算 → CAS 重试”:
CAS(内存地址 V, 期望值 A, 新值 B):
if (V 当前值 == A) {
V = B;
return true; // 交换成功
} else {
return false; // 交换失败,V 不动
}
V:要操作的内存地址(变量)。
A:期望值,调用方认为 V 现在应该是多少。
B:想写进去的新值。
只有V 当前值 == A时才写入 B;否则什么都不做,返回失败。
3、synchronized
synchronized是 JVM 直接支持的关键字(不是类库),提供三件事:
互斥:同一时刻只有一个线程能持有锁、进入临界区。
可见性:线程释放锁前对共享变量的修改,会刷新到主存;下一个拿到锁的线程能看到这些修改(相当于自带
volatile的可见性语义)。可重入:同一线程可以多次获取同一把锁,不会把自己锁死。
4、红黑树
红黑树是带颜色规则的自平衡二叉搜索树:五条规则(尤其“不能红红相连”和“黑高相等”)保证树高始终 O(log n),不会退化成链表;插入/删除靠变色 + 旋转修复,代价是常数次旋转,所以增删性能好,被TreeMap、HashMap/ConcurrentHashMap的桶广泛使用。