目录
ConcurrentHashMap 实现原理:深度解析
一、引言
二、ConcurrentHashMap 的存储结构(JDK1.8)
三、ConcurrentHashMap 的并发安全实现
四、ConcurrentHashMap 的性能优化
(一)锁粒度优化
(二)引入红黑树
(三)并发扩容机制
(四)元素个数统计优化
在面试中,ConcurrentHashMap 的实现原理是一个常考的知识点。不少小伙伴都对这个问题感到困惑,今天我们就来详细剖析一下,帮助大家在面试中脱颖而出。同时,我还为大家准备了福利,在评论区置顶处可以免费领取 30 万字的面试文档,涵盖各大一线大厂面试真题及详细解答哦。
ConcurrentHashMap 是由数组、单向链表和红黑树组成的。当我们初始化一个 ConcurrentHashMap 实例时,默认会初始化一个长度为 16 的数组。由于它依然是一个哈希表,所以必然存在哈希冲突问题。它采用链式寻址法来解决哈希冲突,示例如下(这里是简单的 Java 代码模拟哈希冲突的链式存储,与 ConcurrentHashMap 内部原理类似):
import java.util.LinkedList;
class HashEntry {
int key;
String value;
public HashEntry(int key, String value) {
this.key = key;
this.value = value;
}
}
class MyHashMap {
private LinkedList[] table;
private int size;
public MyHashMap(int capacity) {
table = new LinkedList[capacity];
for (int i = 0; i < capacity; i++) {
table[i] = new LinkedList<>();
}
}
public void put(int key, String value) {
int index = key % table.length;
for (HashEntry entry : table[index]) {
if (entry.key == key) {
entry.value = value;
return;
}
}
table[index].add(new HashEntry(key, value));
}
}
当哈希冲突较多时,链表可能会过长,这种情况下,数据元素的查询复杂度会变成 O (n)。因此在 JDK1.8 中引入了红黑树机制,当数组长度大于 64 并且链表长度大于等于 8 时,单向链表就会转化为红黑树。另外,随着 ConcurrentHashMap 的动态扩容,一旦链表长度小于 6,红黑树就会退化成单向链表。
ConcurrentHashMap 本质上是一个哈希表,功能和普通哈希表类似,但它在哈希表的基础上提供了并发安全的实现。其并发安全主要是通过对指定的 node 节点加锁来保证数据更新的安全性。这种在并发性能和数据安全性之间平衡的设计,在很多地方都有类似情况,比如 CPU 的三级缓存、MySQL 的 buffer pool、synchronized 的锁升级等。
在 JDK1.8 中,ConcurrentHashMap 锁的粒度是数组的某一个节点,而在 JDK1.7 中,锁定的是一个 segment,锁的范围更大,因此 JDK1.7 性能更低。
引入红黑树降低了数据查询的时间复杂度,红黑树的时间复杂度是 O (log n)。
当数组长度不够时,ConcurrentHashMap 需要对数组进行扩容。在扩容方面,它引入了多线程并发扩容的机制。简单来说,就是多个线程对原始数据进行分片以后,每个线程负责一个分片的数据迁移,从而提升了扩容过程中的数据迁移效率。以下是一个简单的多线程处理数据的 Java 示例(与 ConcurrentHashMap 并发扩容思想类似):
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class DataMigrationExample {
public static void main(String[] args) {
List dataList = new ArrayList<>();
// 模拟填充数据
for (int i = 0; i < 100; i++) {
dataList.add(i);
}
// 创建一个线程池,这里使用固定大小为4的线程池
ExecutorService executorService = Executors.newFixedThreadPool(4);
int sliceSize = dataList.size() / 4;
for (int i = 0; i < 4; i++) {
int startIndex = i * sliceSize;
int endIndex = (i == 3)? dataList.size() : (i + 1) * sliceSize;
List sliceData = dataList.subList(startIndex, endIndex);
executorService.execute(() -> {
// 这里模拟每个线程对数据分片的处理,比如迁移操作
for (Integer data : sliceData) {
System.out.println(Thread.currentThread().getName() + "处理数据: " + data);
}
});
}
executorService.shutdown();
}
}
ConcurrentHashMap 中有一个 size 方法来获取总的元素个数。在多线程并发场景中,要保证准确性地实现元素个数累加,性能是非常低的。它在这方面的优化主要体现在两个点上:
希望通过这篇文章,大家对 ConcurrentHashMap 的实现原理有更深入的理解,无论是应对面试还是在实际开发中都能有所帮助。咱们下期再见!