ConcurrentHashMap与CopyOnWriteArrayList
本文最后更新于 2026-01-08,文章内容可能已经过时。
Java JUC包中的ConcurrentHashMap(JDK 1.8实现,基于CAS+synchronized+红黑树)适用于读写均衡的高并发场景(如缓存、实时计数器),提供高吞吐量和弱一致性;CopyOnWriteArrayList采用写时复制机制,适合读多写少的场景(如事件监听器、配置管理),实现无锁读但写操作需复制数组导致内存开销大。选型核心依据是读写比例:读写均衡优先ConcurrentHashMap,读多写少(≥10:1)优先CopyOnWriteArrayList,避免过度使用导致性能问题。
一、JUC包概述
Java JUC (Java Util Concurrent) 是Java平台标准版(JDK)的核心并发工具包,包含在java.util.concurrent包中。它提供了丰富的并发API和工具类,用于简化多线程环境下的并发编程,并确保程序在高并发场景下的正确性和性能。
JUC的核心模块包括:
- 原子类(Atomic):基于CAS的无锁并发操作(如AtomicInteger)
- 锁与同步器(Locks):如ReentrantLock、AQS(AbstractQueuedSynchronizer)
- 并发容器(Collections):如ConcurrentHashMap、CopyOnWriteArrayList
- 线程池(Executors):ThreadPoolExecutor、ForkJoinPool
- 同步工具(Tools):CountDownLatch、CyclicBarrier、Semaphore
- 并发流与Future:如CompletableFuture、ForkJoinTask
下面将深度解析两个核心并发容器:ConcurrentHashMap和CopyOnWriteArrayList。
二、ConcurrentHashMap
1. 源码实现原理
JDK 1.7 vs JDK 1.8
| 特性 | JDK 1.7 | JDK 1.8 |
|---|---|---|
| 锁机制 | 分段锁(Segment) | CAS + synchronized + 红黑树 |
| 锁粒度 | 每个Segment独立加锁 | 锁粒度更细,单个桶(bucket)加锁 |
| 扩容机制 | 扩容时整个Segment锁住 | 渐进式扩容,不锁整个表 |
| 数据结构 | 数组 + 链表 | 数组 + 链表/红黑树 |
JDK 1.8实现原理:
- 数据结构:基于数组+链表/红黑树的结构
- 锁机制:
- 采用Node数组存储数据
- 每个数组元素(桶)是一个链表头节点
- 对桶的访问通过synchronized锁住桶头节点
- 当链表长度≥8时,将链表转换为红黑树
- 扩容机制:
- 渐进式扩容:不是一次性锁住整个表
- 分阶段进行:先创建新数组,然后逐步将旧数组元素迁移到新数组
- 迁移过程中允许读操作
- 通过sizeCtl控制扩容过程
扩容过程:
- 为新的容量分配一个新的数组
- 将旧数组中的元素逐个搬移到新数组中
- 搬移过程中依然允许读操作
- 完成扩容后,将新数组替换为当前数组
扩容示意图:
初始状态:[桶0][桶1][桶2][桶3]...
扩容中:[桶0][桶1][桶2][桶3]... [新桶0][新桶1][新桶2][新桶3]...
完成:[新桶0][新桶1][新桶2][新桶3]...
2. 适用场景
1. 高并发缓存系统
- 会话存储、用户信息缓存
- 读多写少的缓存场景(如Redis缓存的Java本地实现)
2. 实时计数器
- API调用统计
- 广告点击率统计
- 交易量统计
3. 状态管理
- 系统状态维护(如服务可用性状态)
- 配置信息的实时更新与查询
4. 数据分片处理
- 日志分析中的数据分片
- 大数据处理中的任务分配
3. 代码示例
基本用法
import java.util.concurrent.ConcurrentHashMap;
public class ConcurrentHashMapExample {
public static void main(String[] args) {
// 创建ConcurrentHashMap
ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();
// 插入数据
map.put("a", 1);
map.putIfAbsent("a", 2); // 不会覆盖已存在的值
// 原子计算
map.compute("a", (k, v) -> v + 1); // a=2
// 合并计数器
map.merge("count", 1, Integer::sum);
// 遍历(弱一致性)
map.forEach((k, v) -> System.out.println(k + ": " + v));
}
}
高并发计数器实现
public class Counter {
private final ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();
public void increment(String key) {
map.merge(key, 1, Integer::sum);
}
public int getCount(String key) {
return map.getOrDefault(key, 0);
}
// 多线程测试
public static void main(String[] args) throws InterruptedException {
Counter counter = new Counter();
int threadCount = 10;
int incrementCount = 10000;
Thread[] threads = new Thread[threadCount];
for (int i = 0; i < threadCount; i++) {
threads[i] = new Thread(() -> {
for (int j = 0; j < incrementCount; j++) {
counter.increment("total");
}
});
threads[i].start();
}
for (Thread thread : threads) {
thread.join();
}
System.out.println("Total count: " + counter.getCount("total"));
}
}
嵌套ConcurrentHashMap(广告投放系统示例)
public class AdStatsManager {
private final ConcurrentHashMap<String, ConcurrentHashMap<String, WindowStats>> messageCache =
new ConcurrentHashMap<>();
public void updateStats(String slotId, String currentMinute, AdBehaviorDTO adBehaviorDTO) {
messageCache.computeIfAbsent(slotId, k -> new ConcurrentHashMap<>())
.computeIfAbsent(currentMinute, k -> new WindowStats())
.addBehavior(adBehaviorDTO);
}
// WindowStats类
public static class WindowStats {
private int successCount = 0;
private int totalCount = 0;
public void addBehavior(AdBehaviorDTO behavior) {
totalCount++;
if (behavior.isSuccess()) {
successCount++;
}
}
public double getSuccessRate() {
return totalCount == 0 ? 0 : (double) successCount / totalCount;
}
}
// AdBehaviorDTO类
public static class AdBehaviorDTO {
private boolean success;
public AdBehaviorDTO(boolean success) {
this.success = success;
}
public boolean isSuccess() {
return success;
}
}
}
4. 优缺点分析
优点:
- 读操作无锁,性能高
- 写操作通过细粒度锁保证并发安全
- 渐进式扩容,避免性能骤降
- JDK 1.8优化后性能显著提升
缺点:
- 弱一致性:迭代器不反映实时数据
- 内存占用高:红黑树、计数器分片带来开销
- 扩容成本:数据迁移可能暂时阻塞写操作
注意事项:
- 确保key的hashCode()和equals()正确实现
- 合理设置初始容量和负载因子,减少扩容次数
- 避免跨桶的复合操作导致死锁(尽管概率低)
三、CopyOnWriteArrayList
1. 源码实现原理
写时复制(Copy-On-Write)机制:
- 读操作直接访问当前数组,无需同步
- 写操作(add、set、remove)时:
- 加锁(ReentrantLock)
- 复制底层数组
- 在新数组上执行修改
- 替换volatile数组引用
底层实现细节:
public class CopyOnWriteArrayList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable {
private transient volatile Object[] array; // 底层数组
public boolean add(E e) {
final ReentrantLock lock = this.lock;
lock.lock();
try {
Object[] elements = getArray();
int len = elements.length;
Object[] newElements = Arrays.copyOf(elements, len + 1);
newElements[len] = e;
setArray(newElements);
return true;
} finally {
lock.unlock();
}
}
// 读操作
public E get(int index) {
return getArray()[index];
}
}
2. 适用场景
核心条件:数据读取频率远高于写入频率(读写比例≥10:1)
1. 事件监听器列表
- GUI事件系统:监听器注册后极少修改,但事件触发频繁读取
- 事件总线:事件处理器注册后很少变动,但事件触发频繁
2. 配置管理
- 白名单/黑名单管理:配置加载后极少更新,但高频读取
- 系统参数配置:配置文件读入后很少修改,但频繁查询
3. 缓存快照
- 临时缓存:需要提供只读视图,允许短暂的数据延迟
- 状态快照:系统状态的临时快照,用于监控或日志记录
4. 读多写少的集合
- 系统常量列表:如国家代码列表、货币代码列表
- 只读数据集合:如字典、词典
3. 代码示例
基本操作
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.Iterator;
public class CopyOnWriteArrayListExample {
public static void main(String[] args) {
// 初始化
CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();
// 添加元素(写操作触发复制)
list.add("Java");
list.add("Python");
// 读取元素(无锁)
System.out.println(list.get(0)); // 输出 "Java"
// 迭代器基于快照遍历
Iterator<String> it = list.iterator();
list.add("C++"); // 修改列表
// 迭代器只输出Java、Python,不包含C++
while (it.hasNext()) {
System.out.println(it.next());
}
}
}
多线程安全示例
import java.util.concurrent.CopyOnWriteArrayList;
public class ThreadSafeListExample {
private final CopyOnWriteArrayList<Integer> dataCache = new CopyOnWriteArrayList<>();
public void addData(int newData) {
dataCache.add(newData);
}
public void printData() {
for (int num : dataCache) {
System.out.println("Read: " + num);
}
}
public static void main(String[] args) {
ThreadSafeListExample example = new ThreadSafeListExample();
// 读线程(可并发执行)
Thread readerThread = new Thread(() -> {
while (true) {
example.printData();
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
// 写线程(低频触发)
Thread writerThread = new Thread(() -> {
for (int i = 0; i < 10; i++) {
example.addData(i);
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
readerThread.start();
writerThread.start();
}
}
4. 优缺点分析
优点:
- 无锁读:读操作无需同步,性能远超synchronizedList或Vector
- 线程安全:写操作通过复制机制避免数据竞争
- 迭代安全:迭代器遍历期间,即使其他线程修改列表,也不会抛出ConcurrentModificationException
缺点:
- 内存开销大:写操作时存在临时内存翻倍,频繁修改可能导致GC压力
- 数据可能过时:读操作看到的是修改前的快照,不是最新数据
- 写操作性能低:每次写操作都需要复制整个数组
适用场景总结:
| 场景 | 说明 | 读写比例 |
|---|---|---|
| 事件监听器列表 | 监听器注册后极少修改,但事件触发频繁读取 | ≥10:1 |
| 配置管理 | 配置加载后极少更新,但高频读取 | ≥10:1 |
| 缓存快照 | 缓存数据的只读视图,允许短暂的数据延迟 | ≥10:1 |
| 系统常量列表 | 如国家代码、货币代码,加载后很少修改 | ≥10:1 |
四、其他重要JUC并发容器简介
1. ConcurrentHashMap vs CopyOnWriteArrayList
| 特性 | ConcurrentHashMap | CopyOnWriteArrayList |
|---|---|---|
| 读写模式 | 读写并发 | 读无锁,写复制 |
| 数据一致性 | 弱一致性 | 读操作看到的是快照 |
| 适用场景 | 高并发读写 | 读多写少 |
| 内存开销 | 中等 | 高(写时复制) |
| 读性能 | 高 | 极高 |
| 写性能 | 中等 | 低(复制数组) |
| 迭代器 | 弱一致性 | 基于快照 |
2. ForkJoinPool
专为分治算法设计的线程池,核心优势是工作窃取算法:
- 任务分解:将大任务递归拆分为小任务
- 工作窃取:空闲线程从其他线程队列尾部窃取任务
- 适用场景:计算密集型任务,如大数据分析、图像处理
3. ConcurrentLinkedQueue
基于CAS的无锁队列,适用于高并发场景:
ConcurrentLinkedQueue<String> queue = new ConcurrentLinkedQueue<>();
queue.offer("item");
String item = queue.poll();
五、总结与选型建议
1. 选择并发容器的决策树
-
如果需要高并发读写:
- 读写比例高(>10:1) → CopyOnWriteArrayList
- 读写比例均衡 → ConcurrentHashMap
- 计算密集型任务 → ForkJoinPool
-
如果需要线程安全的列表:
- 读多写少 → CopyOnWriteArrayList
- 读写均衡 → Collections.synchronizedList()
- 需要阻塞队列 → BlockingQueue
-
如果需要线程安全的Map:
- 读多写少 → ConcurrentHashMap
- 读写均衡 → Collections.synchronizedMap()
- 需要有序性 → ConcurrentSkipListMap
2. 最佳实践
- 合理设置初始容量:避免频繁扩容
- 选择合适的并发容器:根据业务场景选择,不要过度使用
- 避免在循环中频繁修改:对于CopyOnWriteArrayList,写操作成本高
- 注意弱一致性:理解ConcurrentHashMap的弱一致性特性
- 性能测试:在实际场景中测试不同容器的性能
3. 源码学习建议
- 从简单开始:先理解AtomicInteger的CAS实现
- 逐步深入:研究AQS(AbstractQueuedSynchronizer)框架
- 对比版本:比较JDK 1.7和1.8中ConcurrentHashMap的差异
- 实际应用:在项目中尝试使用这些并发容器,观察性能变化
- 源码调试:使用IDE调试JUC源码,理解关键方法执行流程
六、结论
Java JUC包提供了丰富的并发工具,其中ConcurrentHashMap和CopyOnWriteArrayList是两个非常重要的并发容器。理解它们的实现原理、适用场景和性能特点,对编写高性能、线程安全的Java程序至关重要。
- ConcurrentHashMap适用于读写均衡的高并发场景,是缓存、计数器等应用的首选
- CopyOnWriteArrayList适用于读多写少的场景,能提供极高的读性能,但写操作成本较高
在实际项目中,应根据具体业务场景和性能需求,选择合适的并发容器,避免过度使用或错误使用,从而实现最佳的性能和可维护性。
- 感谢你赐予我前进的力量

