目录
- 引言
- 一、为什么需要ConcurrentHashMap?
- 二、JDK 1.7:分段锁的智慧
- 三、JDK 1.8:CAS + synchronized的进化
- 四、核心API与代码实践
- 五、总结与最佳实践
- 互动环节
引言
在Java并发编程中,HashMap是线程不安全的,而Hashtable又是通过简单粗暴的synchronized方法实现的线程安全,性能堪忧。如何在保证线程安全的同时,又能享受高效的读写操作?
ConcurrentHashMap(简称CHM)便是JDK为我们提供的完美答案!它是JUC包中最耀眼明星之一,也是面试中高频考点。本文将深入浅出,带你彻底掌握CHM的设计精髓与实战应用。
一、为什么需要ConcurrentHashMap?
在多线程环境下,直接使用HashMap进行put操作可能会导致死循环、数据丢失等严重问题。而使用Hashtable或
Collections.synchronizedMap(),虽然能保证线程安全,但却是以全局锁的方式,让所有读写操作串行化,在高并发场景下性能极其低下。
ConcurrentHashMap的设计目标:
- 线程安全:绝对保证多线程下的数据一致性。
- 高性能:读操作通常无需加锁,写操作使用细粒度锁,极大提升并发度。
- 高 scalability(可伸缩性):随着线程数量的增加,性能表现依然良好。
二、JDK 1.7:分段锁的智慧
在JDK 1.7中,CHM采用了分段锁(Segment) 的设计思想,这是一种“锁分离”技术的实现。
你可以把它想象成一个大型停车场:
- 整个停车场(ConcurrentHashMap)被划分成了多个区域(Segment)。
- 每个区域有自己的一把独立门锁(ReentrantLock)。
- 当你要去停车(put一个元素)时,只需要锁住你要停的那个区域即可。
- 其他人可以同时访问其他未被锁住的区域,大大提高了停车场的并发利用率。
数据结构:
// JDK 1.7 近似结构(帮助理解) ConcurrentHashMap { final Segment<K,V>[] segments; // segments数组(多个区域) static class Segment<K,V> extends ReentrantLock { HashEntry<K,V>[] table; // 每个Segment内部又是一个小的HashMap(链地址法) } }
优缺点:
- 优点:相比Hashtable,锁粒度更细,性能提升巨大。
- 缺点:结构略显复杂,在某些场景下仍存在锁竞争。
三、JDK 1.8:CAS + synchronized的进化
JDK 1.8对CHM进行了彻底的重构,摒弃了分段锁,采用了更高效、更现代的实现方式:CAS + synchronized 同步块。
核心变革:
- 数据结构与HashMap趋同:采用 Node数组 + 链表 + 红黑树 的结构。当链表长度超过一定阈值(默认为8)时,会转换为红黑树,极大提升了查找效率。
- 锁的粒度更细:锁的级别从Segment段降低到了数组的每个桶(bucket)的头节点。并发度理论上最高可达数组的长度。
- 使用CAS实现无锁化:对于数组元素的赋值、计数器累加等操作,使用CAS(Compare-And-Swap)这种乐观锁来实现,避免了线程阻塞,效率极高。
- ** synchronized 的优化**:JDK 1.6之后对synchronized做了大量优化(如偏向锁、轻量级锁、锁消除、锁粗化等),其性能已经与ReentrantLock相差无几,且是JVM内置锁,无需引入新的依赖。
put操作流程简析:
- 计算key的hash,定位到数组下标。
- 如果桶为空,采用CAS操作将新节点插入。如果CAS成功,则插入结束;如果失败,说明发生了竞争,进入下一轮循环。
- 如果桶不为空(可能是链表或红黑树),则对该桶的头节点使用synchronized加锁。
- 遍历链表,更新已有节点或新增节点。
- 判断是否需要将链表树化为红黑树。
- 最后检查是否需要扩容。
这种设计使得JDK 1.8的CHM在高并发读写的场景下性能表现远超1.7版本。
四、核心API与代码实践
让我们通过代码来看看CHM的基本用法和一些高级特性。
1. 基本使用
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)); } }
2. 高级特性:原子性复合操作
传统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等方法都是原子操作的,它们内部已经处理好了锁的获取和释放,让我们可以用简洁的代码实现线程安全的复杂逻辑。
3. 性能优化:使用LongAdder
在诸如统计计数的场景下,如果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。
五、总结与最佳实践
- 演进史:JDK 1.7的分段锁 -> JDK 1.8的 CAS + synchronized锁桶头节点。设计越来越简单,性能却越来越高。
- 核心思想:降低锁粒度、无锁化CAS操作、利用现代JVM的锁优化。
- 为何高效:
- 读操作:通常完全无锁(volatile读),速度极快。
- 写操作:只在操作特定桶时加锁,极大减少了锁竞争。
- 最佳实践:
- 默认选择:在多线程环境下需要Map时,应首选ConcurrentHashMap。
- 善用原子方法:多使用computeIfAbsent、merge、forEach等原子方法,它们比你自己加锁更安全、更高效。
- 值的选择:对于频繁更新的计数器,考虑使用LongAdder作为value。
- 大小设置:创建时可以预估数据量,通过initialCapacity和loadFactor参数合理设置初始大小,避免频繁扩容带来的性能损耗。
ConcurrentHashMap是Java并发编程中集智慧和实用性的典范之作。理解其内部机制,不仅能帮助我们在面试中脱颖而出,更能让我们在实际项目中写出既正确又高性能的代码。