ConcurrentHashMap
ConcurrentHashMap,的设计思路、设计原因以及它旨在解决的问题。
1. 解决的问题
ConcurrentHashMap是为了解决HashMap在多线程环境下使用时存在的线程不安全问题而设计的。普通的HashMap在多个线程同时进行读写操作(尤其是扩容时)会导致数据不一致、死循环等问题。传统的线程安全替代方案如Hashtable或Collections.synchronizedMap(),虽然保证了线程安全,但采用了全局锁机制(对整个 Map 加锁),导致在高并发场景下性能急剧下降,所有操作都必须串行执行。
ConcurrentHashMap的目标是在保证线程安全的前提下,提供高并发、高性能的访问能力。
2. 核心设计思路:降低锁的粒度
ConcurrentHashMap的核心设计哲学是分段锁或细粒度锁。其基本思想是将整个 Map 的数据结构划分成多个小的、独立的段。线程在操作时,只需要锁住它当前正在操作的段,而不是锁住整个 Map。这样,多个线程可以同时操作不同的段,从而实现了并发访问。
Java 7 的实现:分段锁 (Segment)
在 Java 7 中,ConcurrentHashMap内部维护了一个Segment数组。每个Segment本质上是一个小的HashMap,并拥有自己独立的锁 (ReentrantLock)。
- 结构:可以想象成一个数组,数组的每个元素是一个
Segment,每个Segment内部又是一个数组(桶)加链表的结构。 - 锁机制:
- 写操作:当对一个键值对进行
put,remove等操作时,首先根据键的哈希值确定它属于哪个Segment,然后只锁住这个特定的Segment。其他Segment仍然可以被其他线程访问。 - 读操作:大多数读操作(如
get)不需要加锁,使用了volatile变量来保证可见性(在 Java 5+ 的内存模型下)。
- 写操作:当对一个键值对进行
- 优点:显著提升了并发度。理论上,最多可以有等于
Segment数量的线程同时进行写操作。 - 缺点:
Segment的数量在创建时就固定了,后期不能调整。如果并发线程数远超过Segment数量,某些Segment上的竞争可能依然激烈。查询操作(如size(),containsValue())可能需要遍历所有Segment,效率不高。
Java 8 及以后的实现:synchronized+CAS+volatile
Java 8 对ConcurrentHashMap进行了重大重构,摒弃了Segment分段锁结构,采用了更细粒度的锁策略:
- 结构:与
HashMap类似,采用桶数组 + 链表/红黑树的结构(解决哈希冲突)。 - 锁机制:
volatile变量:数组引用Node[] table和节点中的value都用volatile修饰,保证内存可见性。CAS(Compare-And-Swap):用于无锁化的、低竞争情况下的快速更新操作(例如,初始化数组、设置数组头节点、计数器的更新等)。CAS是一种乐观锁,通过硬件指令实现,开销较小。synchronized:当CAS失败(表示存在竞争)时,或者操作需要锁定整个桶时(如向链表尾部添加节点、树化操作等),只对链表的头节点(或树的根节点)进行synchronized加锁。锁的粒度是单个桶。- 例如,
put操作:首先计算哈希定位到桶,尝试用CAS插入新节点到链表头(如果桶为空),失败则对该桶的头节点加synchronized锁,然后在链表或红黑树中执行插入。
- 例如,
- 优点:
- 锁粒度更细:锁的是单个桶,而不是整个段。并发度理论上可以达到桶的数量(远多于之前的
Segment数量)。 - 性能更好:
synchronized在 JDK 6 之后进行了大量优化(偏向锁、轻量级锁、锁消除、锁粗化、适应性自旋等),性能已接近ReentrantLock,且synchronized是 JVM 内置锁,无需显式创建锁对象。 - 数据结构优化:当桶中的链表长度超过阈值(默认为 8)且桶数组长度达到一定值(默认为 64)时,链表会转换为红黑树 (
TreeNode),将查找时间复杂度从O(n)O(n)O(n)降为O(logn)O(\log n)O(logn),提高了查找效率。 - 并发度自适应:桶的数量会根据负载因子动态扩容,锁的数量也随之动态变化。
- 计数:使用
LongAdder或类似思想(CounterCell[])来统计元素个数 (size),避免单个计数器的热点竞争。
- 锁粒度更细:锁的是单个桶,而不是整个段。并发度理论上可以达到桶的数量(远多于之前的
3. 为什么这样设计?
- 解决
Hashtable/SynchronizedMap的全局锁瓶颈:通过分段锁或桶锁,允许多个线程同时修改不同部分的数据,极大地提高了并发性能。 - 利用现代硬件和 JVM 特性:Java 8 的实现充分利用了
volatile的内存语义、CAS指令的高效无锁操作,以及现代 JVM 对synchronized的深度优化。 - 适应高并发场景:现代应用对并发性能要求极高,
ConcurrentHashMap的设计使其成为并发编程中Map实现的默认首选。 - 平衡安全与性能:在保证线程安全(可见性、原子性)的前提下,尽可能减少锁的持有时间和范围,最大化并发度。
4. 关键特性总结
- 线程安全:保证多线程环境下的正确性。
- 高并发性:通过细粒度锁或无锁操作 (
CAS),允许多线程并发读写。 - 弱一致性:迭代器、
size、isEmpty等方法反映的是某个瞬间的状态,不保证强一致性(但这通常是可接受的)。 - 不会抛出
ConcurrentModificationException:迭代过程中允许修改。 - 高效的批量操作:提供了
forEach,search,reduce等并行操作的方法。
适用场景
ConcurrentHashMap非常适合需要高并发读写访问Map的场景,例如:
- 缓存实现。
- 共享配置存储。
- 需要频繁并发更新的计数器。
- 多线程共享数据的容器。
总之,ConcurrentHashMap通过精妙的分段锁(Java 7)和更先进的synchronized+CAS+volatile+ 红黑树(Java 8+)的设计,在保证线程安全的前提下,提供了卓越的并发性能,是 Java 并发编程库中一个非常重要的组件。
