Java 中的 ConcurrentHashMap。这是一个在并发编程中极其重要且性能优越的类,理解其原理和用法至关重要。
1. 概述与核心思想
ConcurrentHashMap(以下简称 CHM)是 Java 并发包 (java.util.concurrent) 中提供的一个高性能、线程安全的哈希表实现。它旨在解决 HashMap 在多线程环境下不安全和 Hashtable(以及使用 Collections.synchronizedMap 包装的 HashMap)在高并发下性能瓶颈的问题。
-
HashMap:非线程安全。多线程同时 put 可能导致链表成环,进而引起 CPU 100% 等问题。 -
Hashtable:线程安全,但实现方式简单粗暴——在所有方法上使用synchronized关键字进行同步。这导致所有线程在竞争同一把锁,性能极差。 -
ConcurrentHashMap:采用“锁分段”技术(Java 7)或“CAS + synchronized”技术(Java 8),大幅降低了锁的粒度,允许多个线程并发地进行读写操作,从而获得了极高的并发性能。
核心设计目标:在保证线程安全的前提下,尽可能地提高并发访问的效率。
2. 版本演进:Java 7 vs Java 8
CHM 的实现原理在 Java 7 和 Java 8 中有重大变化,理解这个演进过程能更好地掌握其精髓。
Java 7 实现(分段锁机制 - Segment)
Java 7 中的 CHM 使用了一个叫做 分段锁 (Segment) 的结构。
-
数据结构:
-
它内部维护了一个
Segment数组。每个Segment本质上就是一个继承了ReentrantLock的哈希表。 -
每个
Segment又包含了一个HashEntry数组(可以理解为一个小型的HashMap)。 -
所以,整个 CHM 的结构可以看作是一个二级哈希:第一级是
Segment数组,第二级是HashEntry数组。
-
-
并发机制:
-
锁的粒度:锁不是加在整个 CHM 上,而是加在每个
Segment上。 -
当需要 put 一个元素时,CHM 会首先根据 key 的哈希值找到对应的
Segment,然后只锁定这个特定的Segment。 -
其他线程可以同时访问和修改其他
Segment上的数据。 -
默认情况下,
Segment的数量是 16(并发级别concurrencyLevel),这意味着理想情况下,最多可以有 16 个线程同时执行写操作,比Hashtable的任何时候只能有一个线程写,性能提升了16倍。
-
-
优缺点:
-
优点:相比
Hashtable,锁粒度更细,并发性能大幅提升。 -
缺点:实现相对复杂。在某些需要跨段锁定的操作(如
size())时,仍然需要按顺序尝试锁定所有段,效率不高。
-
Java 8 实现(CAS + synchronized)
Java 8 对 CHM 进行了彻底的重构,摒弃了分段锁的设计,采用了与 HashMap 更相似的数据结构(数组+链表/红黑树)和更先进的并发控制技术。
-
数据结构:
-
类似于
HashMap,使用Node数组作为桶数组。Node可以形成链表或红黑树(当链表长度超过阈值时,默认 >8 且 数组长度>=64 时转换)。
-
-
并发机制:
-
CAS (Compare-And-Swap):这是一种无锁算法,用于实现乐观锁。在插入新节点(
put)时,如果发现目标桶位是空的(null),会使用 CAS 操作来尝试写入新节点,避免了直接加锁。 -
synchronized:如果 CAS 失败(说明有其他线程正在操作),或者目标桶位不为空(已有链表或树),则会对这个桶位的头节点(第一个Node对象) 使用
synchronized进行加锁。 -
锁的粒度:锁的粒度从 Java 7 的 一段(多个桶) 细化到了 Java 8 的 一个桶(一个链表/树的头节点)。这意味着,只要多个线程操作的不是同一个桶,它们就可以完全并发执行,并发度理论上等于桶数组的大小(可以非常大,比如上万个),比 Java 7 的 16 段高出几个数量级。
-
-
优点:
-
锁粒度更细:并发度极高。
-
实现更简洁:数据结构与
HashMap统一,代码更易理解和维护。 -
性能进一步提升:尤其在低冲突的场景下,CAS 操作效率很高。
-
扩展了API:提供了大量支持函数式编程的原子性操作(如
compute,merge等)。
-
总结版本差异:从“分段锁”到“桶级别锁 (CAS + synchronized)”,锁的粒度更细,并发性能更高,数据结构更现代化。
3. 关键特性
-
线程安全:所有操作都是线程安全的,无需额外同步。
-
弱一致性迭代器 (Weakly Consistent Iterators):
-
这是 CHM 与加锁容器(如
Hashtable)的一个关键区别。 -
当使用迭代器遍历 CHM 时,它不会抛出
ConcurrentModificationException。 -
迭代器反映的是创建它时(或之后)的某个时间点的状态。它可能不会反映出迭代过程中所有最新的修改,但也不会将同一个元素返回两次。
-
这种设计避免了在遍历时需要锁定整个映射,从而保证了高性能。
-
-
高效的并发操作:
get操作通常是无锁的(volatile read),因此效率极高。put、remove等写操作只在必要时才加锁。 -
不允许 null 键或 null 值:这是设计上的决定。在并发环境下,如果
get(key)返回null,你无法区分是 key 不存在,还是 key 对应的 value 本身就是null。这会引起歧义,因此直接禁止。
4. 重要方法及使用
除了从 Map 接口继承的标准方法(如 get, put, remove),CHM 提供了一系列非常实用的原子性复合操作方法,这些是它的精髓所在:
ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();
// 1. putIfAbsent(K key, V value)
// 如果key不存在,则put值。等价于(但它是原子的):
// if (!map.containsKey(key)) return map.put(key, value);
// else return map.get(key);
Integer previous = map.putIfAbsent("key", 1);
// 2. remove(Object key, Object value)
// 只有当key映射的value等于给定的value时,才移除该条目。
// 原子性地等价于:
// if (map.get(key).equals(value)) {
// map.remove(key);
// return true;
// } else return false;
boolean removed = map.remove("key", 1);
// 3. replace(K key, V oldValue, V newValue)
// 只有当key映射的value等于给定的oldValue时,才将其替换为newValue。
boolean replaced = map.replace("key", 1, 2);
// 4. compute 系列方法 (Java 8+)
// compute(K key, BiFunction<? super K,? super V,? extends V> remappingFunction)
// 原子性地计算指定key的新value。
map.compute("key", (k, oldVal) -> oldVal == null ? 1 : oldVal + 1);
// computeIfAbsent(K key, Function<? super K,? extends V> mappingFunction)
// 如果key不存在或其value为null,则使用函数计算value并put。
// 常用于“如果不存在则创建”的场景,如初始化缓存、创建惰性集合。
Map<String, List<String>> multiValueMap = new ConcurrentHashMap<>();
List<String> list = multiValueMap.computeIfAbsent("names", k -> new CopyOnWriteArrayList<>());
list.add("Alice");
// computeIfPresent(K key, BiFunction<? super K,? super V,? extends V> remappingFunction)
// 如果key存在且value不为null,则使用函数计算新value。
// 5. merge(K key, V value, BiFunction<? super V,? super V,? extends V> remappingFunction)
// 原子性地合并操作。如果key不存在,直接put(value);如果存在,则用remappingFunction合并旧值和新值。
map.merge("count", 1, Integer::sum); // 原子性的计数器实现!
// 6. forEach / search / reduce (Java 8+)
// 支持并行遍历和操作的批量方法。
map.forEach(2, // parallelismThreshold 并行度阈值
(k, v) -> System.out.println(k + " -> " + v)); // 转换器
(k, v) -> v > 10); // 过滤器
5. 使用场景与最佳实践
-
典型场景:
-
高并发缓存:这是 CHM 最经典的应用。例如,Guava 的
Cache底层就使用了 CHM。 -
共享数据存储:需要被多个线程频繁读写的共享键值对数据。
-
替代
Hashtable和synchronizedMap:在任何需要线程安全 Map 且对性能有要求的场景。
-
-
注意事项:
-
size()、isEmpty()、containsValue()这些方法返回的是估计值,因为它们在计算时可能正有其他线程在修改映射。它们通常用于监控和调试,而非程序逻辑控制。 -
迭代器是弱一致性的,不要指望它能获取到遍历过程中所有的最新修改。
-
虽然锁粒度很细,但还是要避免在一个锁持有时间内执行耗时操作(如IO),否则会阻塞其他需要操作同一个桶的线程。
-
合理设置初始容量和负载因子,以减少扩容(
resize)带来的开销,这与HashMap的优化思路一致。
-
总结
| 特性 | Java 7 ConcurrentHashMap | Java 8+ ConcurrentHashMap |
|---|---|---|
| 数据结构 | 分段数组 + 链表 | 数组 + 链表/红黑树 |
| 锁机制 | 分段锁 (ReentrantLock) | 桶级别锁 (CAS + synchronized) |
| 锁粒度 | 较细(段,默认16) | 极细(单个桶的头节点) |
| 并发度 | 受段数限制 | 理论上受桶数限制,非常高 |
| API | 基础并发方法 | 丰富的函数式API (compute, forEach等) |
ConcurrentHashMap 是 Java 并发编程工具集中的一个杰作,它巧妙地利用了现代硬件的特性(CAS)和 JVM 的优化(synchronized 锁升级),在线程安全和性能之间取得了完美的平衡。理解其原理有助于你编写出更高效、更健壮的多线程程序。
2945

被折叠的 条评论
为什么被折叠?



