神话力量免安装绿色中文版
12.2G · 2025-10-26
在Java并发编程中,HashMap是线程不安全的,而Hashtable又是通过简单粗暴的synchronized方法实现的线程安全,性能堪忧。如何在保证线程安全的同时,又能享受高效的读写操作?
ConcurrentHashMap(简称CHM)便是JDK为我们提供的完美答案!它是JUC包中最耀眼明星之一,也是面试中高频考点。本文将深入浅出,带你彻底掌握CHM的设计精髓与实战应用。
在多线程环境下,直接使用HashMap进行put操作可能会导致死循环、数据丢失等严重问题。而使用Hashtable或
Collections.synchronizedMap(),虽然能保证线程安全,但却是以全局锁的方式,让所有读写操作串行化,在高并发场景下性能极其低下。
ConcurrentHashMap的设计目标:
在JDK 1.7中,CHM采用了分段锁(Segment) 的设计思想,这是一种“锁分离”技术的实现。
你可以把它想象成一个大型停车场:
数据结构:
// JDK 1.7 近似结构(帮助理解)
ConcurrentHashMap {
final Segment<K,V>[] segments; // segments数组(多个区域)
static class Segment<K,V> extends ReentrantLock {
HashEntry<K,V>[] table; // 每个Segment内部又是一个小的HashMap(链地址法)
}
}
优缺点:
JDK 1.8对CHM进行了彻底的重构,摒弃了分段锁,采用了更高效、更现代的实现方式:CAS + synchronized 同步块。
核心变革:
put操作流程简析:
这种设计使得JDK 1.8的CHM在高并发读写的场景下性能表现远超1.7版本。
让我们通过代码来看看CHM的基本用法和一些高级特性。
import java.util.concurrent.ConcurrentHashMap;
public class CHMDemo {
public static void main(String[] args) {
// 1. 创建ConcurrentHashMap
ConcurrentHashMap<String, Integer> scores = new ConcurrentHashMap<>();
// 2. put - 线程安全的放入键值对
scores.put("Alice", 100);
scores.put("Bob", 90);
// 3. get - 线程安全的获取(读操作通常无锁,效率极高)
Integer aliceScore = scores.get("Alice");
System.out.println("Alice's score: " + aliceScore); // Output: 100
// 4. computeIfAbsent - 如果key不存在,则通过函数计算value并放入Map
// 这是一个原子操作,非常适合用于"懒加载"场景
scores.computeIfAbsent("Charlie", key -> {
System.out.println("Calculating score for " + key);
return 80; // 这个Lambda表达式只会执行一次
});
System.out.println(scores.get("Charlie")); // Output: 80
// 5. forEach - 并发安全的遍历(JDK 8+)
// 注意:这里不会抛出ConcurrentModificationException!
scores.forEach((key, value) -> System.out.println(key + ": " + value));
}
}
传统HashMap即使加了synchronized,一些复合操作(如“检查再更新”)也不是线程安全的。CHM提供了大量的原子性方法来解决这个问题。
经典问题:单词计数
public class WordCount {
private final ConcurrentHashMap<String, Long> wordCounts = new ConcurrentHashMap<>();
// 线程安全地增加单词计数
public void addWord(String word) {
// 传统方式(丑陋且效率稍低)
// synchronized (wordCounts) {
// Long oldValue = wordCounts.get(word);
// Long newValue = (oldValue == null) ? 1L : oldValue + 1;
// wordCounts.put(word, newValue);
// }
// CHM优雅方式1: compute (JDK 8+)
// wordCounts.compute(word, (key, oldValue) -> oldValue == null ? 1 : oldValue + 1);
// CHM优雅方式2: merge (更简洁,JDK 8+)
wordCounts.merge(word, 1L, (oldValue, value) -> oldValue + value);
// CHM优雅方式3: 使用原子类LongAdder作为值(最优解)
// 详见下文
}
public Long getCount(String word) {
return wordCounts.get(word);
}
}
compute和merge等方法都是原子操作的,它们内部已经处理好了锁的获取和释放,让我们可以用简洁的代码实现线程安全的复杂逻辑。
在诸如统计计数的场景下,如果key非常频繁地被更新,使用AtomicLong作为value仍然会因为多个线程同时CAS更新同一个value而造成竞争。JDK 8提供了LongAdder来极致优化这种场景。
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.LongAdder;
public class OptimalWordCount {
// value 使用 LongAdder
private final ConcurrentHashMap<String, LongAdder> wordCounts = new ConcurrentHashMap<>();
public void addWord(String word) {
// 如果key不存在,则创建一个新的LongAdder;然后让对应的LongAdder自增1
wordCounts.computeIfAbsent(word, key -> new LongAdder()).increment();
}
public long getCount(String word) {
// 获取LongAdder,然后求和
return wordCounts.getOrDefault(word, new LongAdder()).longValue();
}
}
LongAdder内部采用分散热点的思想,将一个value分解成多个cell,线程竞争时操作不同的cell,最后求和得到最终结果。这在超高并发写场景下性能远优于AtomicLong。
ConcurrentHashMap是Java并发编程中集智慧和实用性的典范之作。理解其内部机制,不仅能帮助我们在面试中脱颖而出,更能让我们在实际项目中写出既正确又高性能的代码。