ConcurrentHashMap 的底层原理及是如何实现线程安全的?
一、核心特性
- 线程安全:支持多线程并发访问,无需额外同步措施。
- 高效并发:读写操作分离,读操作无锁(或轻量级锁),写操作仅锁定部分数据,大幅降低锁竞争。
- 功能完备:实现了
ConcurrentMap
接口,支持原子性操作(如putIfAbsent
、remove
等)。
二、JDK 1.7 与 JDK 1.8 的实现差异
ConcurrentHashMap 在不同版本中实现机制差异较大,核心区别如下:
1. JDK 1.7 实现:分段锁(Segment)
结构:由
Segment
数组 +HashEntry
链表组成,每个Segment
本质是一个小的 HashMap。锁机制:
- 每个
Segment
独立加锁(ReentrantLock),不同 Segment 的操作互不干扰。 - 默认有 16 个 Segment,理论上支持 16 个线程同时写入
- 每个
2. JDK 1.8 实现:CAS + synchronized
- 结构:与 HashMap 类似,采用 数组 + 链表 + 红黑树,移除了 Segment。
- 锁机制:
- 读操作:无锁,通过
volatile
保证可见性(读取最新值)。 - 写操作:对链表头节点或红黑树的根节点使用
synchronized
加锁,仅锁定当前桶(Bucket)。 - 利用 CAS(Compare-And-Swap)操作优化无竞争场景下的插入、更新。
- 读操作:无锁,通过
- 优势:锁粒度更细(从 Segment 缩小到桶),并发性能更优。
三、核心操作原理
1. 读操作(get)
- 无需加锁,通过
volatile
修饰的节点引用,直接读取最新数据。 - 若读取过程中发生哈希冲突(链表或红黑树),遍历节点时判断节点是否被修改(通过
volatile
保证可见性)。
2. 写操作(put)
- 计算 key 的哈希值,定位到数组索引(桶)。
- 若桶为空,通过 CAS 操作直接插入新节点。
- 若桶不为空,对桶的头节点加
synchronized
锁,然后执行插入、更新或链表转红黑树操作。 - 插入后检查容量,如需扩容则触发 resize(多线程协作扩容)。
3. 原子操作(如 putIfAbsent)
- 提供
putIfAbsent(key, value)
方法:当 key 不存在时才插入,原子性执行,避免并发下的覆盖问题。 - 实现原理:通过 CAS + 循环重试,或加锁保证操作的原子性。
四、与其他并发容器的对比
容器 | 线程安全实现 | 读性能 | 写性能 | 适用场景 |
---|---|---|---|---|
HashMap | 非线程安全 | 高 | 高 | 单线程环境 |
Hashtable | 全表 synchronized | 低 | 低 | 并发量极低的场景 |
ConcurrentHashMap | JDK1.7 分段锁;JDK1.8 CAS + synchronized | 极高 | 高 | 高并发读写场景(如缓存、计数器) |
五、使用示例
import java.util.concurrent.ConcurrentHashMap;public class ConcurrentHashMapDemo {public static void main(String[] args) {ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();// 插入元素map.put("apple", 1);// 原子操作:不存在则插入map.putIfAbsent("banana", 2);// 读取元素int count = map.get("apple");// 并发迭代(弱一致性,不抛出 ConcurrentModificationException)for (String key : map.keySet()) {System.out.println(key + ": " + map.get(key));}}
}
总结
ConcurrentHashMap 是高并发场景下的首选哈希表实现,其核心优势在于:
- JDK 1.8 采用 CAS + 细粒度 synchronized 锁,大幅提升并发性能;
- 读操作无锁,写操作仅锁单个桶,锁竞争极小;
- 支持原子性操作和弱一致性迭代,避免并发修改异常。
在多线程环境中,推荐使用 ConcurrentHashMap 替代 HashMap(线程不安全)和 Hashtable(效率低)