并发 - ConcurrentHashMap 深度解析
ConcurrentHashMap 深度解析
ConcurrentHashMap是Java并发包(JUC)中最重要的线程安全集合之一,也是面试中关于并发编程的最高频考点。
1. 为什么需要 ConcurrentHashMap?
HashMap 是非线程安全的,在并发环境下使用会导致数据覆盖甚至死循环。虽然可以通过Hashtable或Collections.synchronizedMap(new HashMap<>())来实现线程安全的Map,但这两种方案都是通过对整个Map对象加synchronized重锁来实现的,锁的粒度太大,导致所有操作都必须串行执行,性能极差。
ConcurrentHashMap 的出现就是为了在保证线程安全的同时,提供尽可能高的并发性能。
2. JDK 1.7 vs. JDK 1.8 的实现原理对比
a) JDK 1.7: 分段锁 (Segment Locking)
- 核心思想: 将整个哈希表在逻辑上划分为多个
Segment(默认16个)。每个Segment本身就像一个小型的、线程安全的HashMap,它继承自ReentrantLock,拥有自己独立的锁。 - 数据结构:
ConcurrentHashMap->Segment[]->HashEntry[]->HashEntry链表。 - 工作方式:
- 当一个线程需要对某个数据进行操作时,它只需要锁定该数据所属的那个
Segment,而不会影响其他Segment的操作。 get操作大部分情况下不加锁,通过volatile保证可见性。
- 当一个线程需要对某个数据进行操作时,它只需要锁定该数据所属的那个
- 优点: 相比于对整个
Map加锁,分段锁技术允许多个线程同时访问不同的Segment,并发度大大提高(理论上最高并发度就是Segment的数量)。 - 缺点: 锁的粒度还是比较大(锁住了整个
Segment),在数据分布不均匀或高竞争的情况下,仍然可能出现热点Segment的性能瓶瓶颈。
b) JDK 1.8: CAS + synchronized
- 核心思想: 放弃了
Segment,回归到“数组+链表/红黑树”的基础结构上,通过极大地减小锁的粒度来提高并发性能。 - 数据结构:
Node[]->Node链表 /TreeNode红黑树。 put操作的线程安全机制:- 初始化:如果
Node[]数组(table)为空,通过CAS进行初始化,保证只有一个线程能成功。 - 桶为空:如果要插入的位置(桶)是空的,直接使用
CAS尝试写入新节点,这是无锁的。如果CAS失败,说明有竞争,则进入下一步。 - 桶不为空(发生哈希冲突):
- 如果桶的头节点是链表节点,则使用
synchronized锁住该桶的头节点,然后再进行链表遍历和插入操作。 - 如果桶的头节点是红黑树节点,也同样使用
synchronized锁住头节点。
- 如果桶的头节点是链表节点,则使用
- 初始化:如果
- 优点: 将锁的粒度从“段(Segment)”缩小到了“桶的头节点”,只有在操作同一个哈希桶时才会发生竞争,极大地提高了高竞争下的并发性能。
synchronized锁优化: JDK 1.8中,JVM对synchronized进行了大量优化(偏向锁、轻量级锁、自旋),对于ConcurrentHashMap这种锁粒度小、占用时间短的场景,其性能已经不输于甚至优于ReentrantLock。
3. 核心问题深度解析
a) size() 方法为什么不精确?
ConcurrentHashMap的size()方法返回的是一个估计值,而不是一个实时精确的值。- 原因: 为了保证高性能,
size()方法在计算元素总数时不会对整个Map进行加锁。 - 实现机制 (JDK 1.8):
ConcurrentHashMap内部维护一个baseCount变量和一个CounterCell[]数组来分散计数。put/remove操作会通过CAS来更新这些计数值。size()方法会累加baseCount和所有CounterCell的值。- 由于在
size()方法进行累加的过程中,其他线程可能同时在进行put或remove操作,这些操作对计数值的修改可能不会立即被size()方法观察到,因此size()方法返回的是一个估计值。
- 结论: 这是一个为了高性能和高并发而对数据一致性做出的权衡。如果需要精确的数量(如
mappingCount()方法),在Java 8中虽然比size()更接近精确,但仍然不保证是原子性的,如果需要绝对精确,仍然需要外部加锁。
b) 迭代器为什么不会抛出 ConcurrentModificationException?
- 答案: 不会。
- 原因:
ConcurrentHashMap的迭代器被设计为“弱一致性(Weakly Consistent)”或“故障安全(Fail-Safe)”的。 - 工作机制:
- 当你创建一个迭代器时,它反映的是
ConcurrentHashMap在某个时间点的状态的快照。 - 在迭代过程中,如果其他线程对
Map进行了修改(增、删),迭代器不保证能反映出这些修改,但它也绝不会抛出ConcurrentModificationException。 - 迭代器看到的内容,是基于创建它那一刻的哈希表结构,并且可能会(但不保证)反映出后续的一些修改。
- 当你创建一个迭代器时,它反映的是
- 对比: 这与
HashMap或ArrayList的“快速失败(Fail-Fast)”迭代器形成鲜明对比,后者在迭代时如果发现集合被修改,会立即抛出ConcurrentModificationException。

浙公网安备 33010602011771号