Java ConcurrentHashMap - 详解

 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) 的结构。

  1. 数据结构

    • 它内部维护了一个 Segment 数组。每个 Segment 本质上就是一个继承了 ReentrantLock 的哈希表。

    • 每个 Segment 又包含了一个 HashEntry 数组(可以理解为一个小型的 HashMap)。

    • 所以,整个 CHM 的结构可以看作是一个二级哈希:第一级是 Segment 数组,第二级是 HashEntry 数组。

  2. 并发机制

    • 锁的粒度:锁不是加在整个 CHM 上,而是加在每个 Segment 上。

    • 当需要 put 一个元素时,CHM 会首先根据 key 的哈希值找到对应的 Segment,然后只锁定这个特定的 Segment

    • 其他线程可以同时访问和修改其他 Segment 上的数据。

    • 默认情况下,Segment 的数量是 16(并发级别 concurrencyLevel),这意味着理想情况下,最多可以有 16 个线程同时执行写操作,比 Hashtable 的任何时候只能有一个线程写,性能提升了16倍。

  3. 优缺点

    • 优点:相比 Hashtable,锁粒度更细,并发性能大幅提升。

    • 缺点:实现相对复杂。在某些需要跨段锁定的操作(如 size())时,仍然需要按顺序尝试锁定所有段,效率不高。

Java 8 实现(CAS + synchronized)

Java 8 对 CHM 进行了彻底的重构,摒弃了分段锁的设计,采用了与 HashMap 更相似的数据结构(数组+链表/红黑树)和更先进的并发控制技术。

  1. 数据结构

    • 类似于 HashMap,使用 Node 数组作为桶数组。Node 可以形成链表或红黑树(当链表长度超过阈值时,默认 >8 且 数组长度>=64 时转换)。

  2. 并发机制

    • CAS (Compare-And-Swap):这是一种无锁算法,用于实现乐观锁。在插入新节点(put)时,如果发现目标桶位是空的(null),会使用 CAS 操作来尝试写入新节点,避免了直接加锁。

    • synchronized:如果 CAS 失败(说明有其他线程正在操作),或者目标桶位不为空(已有链表或树),则会对这个桶位的头节点(第一个Node对象) 使用 synchronized 进行加锁。

    • 锁的粒度:锁的粒度从 Java 7 的 一段(多个桶) 细化到了 Java 8 的 一个桶(一个链表/树的头节点)。这意味着,只要多个线程操作的不是同一个桶,它们就可以完全并发执行,并发度理论上等于桶数组的大小(可以非常大,比如上万个),比 Java 7 的 16 段高出几个数量级。

  3. 优点

    • 锁粒度更细:并发度极高。

    • 实现更简洁:数据结构与 HashMap 统一,代码更易理解和维护。

    • 性能进一步提升:尤其在低冲突的场景下,CAS 操作效率很高。

    • 扩展了API:提供了大量支持函数式编程的原子性操作(如 computemerge等)。

总结版本差异:从“分段锁”到“桶级别锁 (CAS + synchronized)”,锁的粒度更细,并发性能更高,数据结构更现代化。


3. 关键特性

  1. 线程安全:所有操作都是线程安全的,无需额外同步。

  2. 弱一致性迭代器 (Weakly Consistent Iterators)

    • 这是 CHM 与加锁容器(如 Hashtable)的一个关键区别。

    • 当使用迭代器遍历 CHM 时,它不会抛出 ConcurrentModificationException

    • 迭代器反映的是创建它时(或之后)的某个时间点的状态。它可能不会反映出迭代过程中所有最新的修改,但也不会将同一个元素返回两次。

    • 这种设计避免了在遍历时需要锁定整个映射,从而保证了高性能。

  3. 高效的并发操作get 操作通常是无锁的(volatile read),因此效率极高。putremove 等写操作只在必要时才加锁。

  4. 不允许 null 键或 null 值:这是设计上的决定。在并发环境下,如果 get(key) 返回 null,你无法区分是 key 不存在,还是 key 对应的 value 本身就是 null。这会引起歧义,因此直接禁止。


4. 重要方法及使用

除了从 Map 接口继承的标准方法(如 getputremove),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 ConcurrentHashMapJava 8+ ConcurrentHashMap
数据结构分段数组 + 链表数组 + 链表/红黑树
锁机制分段锁 (ReentrantLock)桶级别锁 (CAS + synchronized)
锁粒度较细(段,默认16)极细(单个桶的头节点)
并发度受段数限制理论上受桶数限制,非常高
API基础并发方法丰富的函数式API (computeforEach等)

ConcurrentHashMap 是 Java 并发编程工具集中的一个杰作,它巧妙地利用了现代硬件的特性(CAS)和 JVM 的优化(synchronized 锁升级),在线程安全和性能之间取得了完美的平衡。理解其原理有助于你编写出更高效、更健壮的多线程程序。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值