在多线程编程中,普通集合(如ArrayList、HashMap)就像“无锁的粮仓”,多个线程同时读写时容易“粮食混乱”(数据不一致)。Java提供了两种“带锁的高级粮仓”:CopyOnWrite容器和ConcurrentHashMap,它们分别适用于不同的并发场景。今天就用最通俗的语言,带大家搞懂这两个“并发神器”!
一、第一个神器:CopyOnWrite容器(读多写少的“复印笔记本”)
📚 什么是CopyOnWrite?
全称是写时复制(Copy-On-Write),就像你有一本“神奇笔记本”:
- 当多个同学同时“读”笔记时,大家看的都是同一本,互不干扰;
- 当某个同学要“改”笔记时,先悄悄复印一本新的,改完后把旧笔记本换成新的,其他同学下次读时就会看到新内容。
🔑 核心原理:
- 读操作无锁:直接读取原数据,无需加锁(因为读的是旧版本,不会被写操作打断);
- 写操作复制:写入时创建新的副本,修改完后用新副本替换旧版本(类似“替换指针”)。
📖 典型代表:CopyOnWriteArrayList
// 创建一个支持并发的List
List<String> list = new CopyOnWriteArrayList<>();
// 线程A添加元素(写操作:复制新数组)
list.add("苹果");
// 线程B同时遍历(读操作:直接访问旧数组,不会被阻塞)
for (String item : list) {
System.out.println(item);
}
⚡ 优点:
- 读性能超强:读操作不加锁,适合“读多写少”场景(如日志收集、配置列表);
- 遍历安全:遍历时允许其他线程修改,不会抛出ConcurrentModificationException(因为遍历的是副本的快照)。
⚠️ 缺点:
- 写操作慢:每次写都要复制整个容器(比如ArrayList底层是数组,复制数组耗时随数据量增大而增加);
- 数据延迟可见:写操作的新数据,不会立即被正在遍历的线程看到(因为它们读的是旧副本)。
📍 适用场景:
- 读操作远多于写操作(如缓存列表、只读配置);
- 允许“延迟更新”(比如日志先记录到内存,稍后批量处理)。
二、第二个神器:ConcurrentHashMap(高并发的“分区保险柜”)
🏦 从HashTable到ConcurrentHashMap的进化
- HashTable:早期的线程安全Map,直接给整个Map加一把大锁(类似“整个保险柜只有一把钥匙”),并发度低,性能差;
- ConcurrentHashMap(Java 7):引入“分段锁”(Segment),把Map分成16个段(Segment),每个段独立加锁(类似“保险柜分16个抽屉,每个抽屉单独上锁”),并发度提升16倍;
- ConcurrentHashMap(Java 8):进一步优化,用红黑树替代链表(数据量大时查找更快),并引入CAS无锁操作,锁粒度更细(甚至无锁),性能大幅提升。
🔧 Java 8版本核心原理:
- 数组+链表+红黑树:结构和HashMap类似,但支持并发;
- 锁细化:每个链表/红黑树的头节点作为锁(粒度到单个桶),写操作只锁当前桶;
- CAS无锁读:读操作通过volatile变量直接获取,无需加锁(保证可见性)。
💻 代码示例:
// 创建高并发的Map
ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();
// 线程A插入数据(加锁当前桶,不影响其他桶)
map.put("苹果", 100);
// 线程B同时计算大小(无锁读,直接获取全局计数器)
int size = map.size();
🚀 优点:
- 高并发支持:写操作锁粒度极细(仅锁当前桶),读操作几乎无锁,适合“读写频繁”场景;
- 实时性强:写操作的新数据,其他线程立即可见(不像CopyOnWrite需要等副本替换)。
⚠️ 缺点:
- 复杂度高:内部实现复杂(分段锁、红黑树、CAS混合使用),不适合简单场景;
- 不支持完全遍历一致性:遍历时可能会看到部分旧数据(但整体一致性有保证)。
📍 适用场景:
- 高并发读写(如电商实时库存统计、实时计数器);
- 需要“强一致性”(比如订单实时写入,立即查询)。
三、两者对比:到底该选谁?
特性 | CopyOnWrite容器(如ArrayList) | ConcurrentHashMap |
---|---|---|
核心思想 | 写时复制(读旧写新) | 细粒度锁+CAS无锁 |
读性能 | 无锁,极快(适合读多写少) | 无锁,快(读写均衡) |
写性能 | 慢(复制成本高) | 快(仅锁当前桶) |
数据可见性 | 延迟可见(读旧副本) | 立即可见 |
典型场景 | 日志收集、配置列表 | 实时统计、高频读写 |
线程安全级别 | 最终一致性 | 强一致性(部分操作) |
四、避坑指南:这些细节要注意!
1. CopyOnWrite的“隐藏成本”
- 不要在写操作频繁的场景使用(比如每秒上万次写入),复制数组会导致内存飙升和延迟增大;
- 容器的
size()
、contains()
等操作是安全的,但set()
操作(修改元素)仍需加锁(因为会替换整个数组)。
2. ConcurrentHashMap的“特殊方法”
- 慎用
putIfAbsent()
:虽然原子性保证,但高并发下可能因重试导致性能下降; - 遍历建议:用
forEach()
、keySet()
等并发安全的方法,避免直接使用迭代器(可能遍历到旧数据,但不会抛异常)。
3. 基础集合的“线程安全陷阱”
- ArrayList → CopyOnWriteArrayList(读多写少);
- HashMap → ConcurrentHashMap(读写频繁);
- HashTable:已过时,除非兼容旧代码,否则永远不要用!
五、总结:选对工具,事半功倍!
- 如果你需要“读多写少+遍历安全”:选CopyOnWrite容器(比如日志记录、只读缓存);
- 如果你需要“高并发读写+实时性”:选ConcurrentHashMap(比如实时计数器、电商库存);
- 核心原则:根据读写比例和数据可见性要求选择——读多且允许延迟选前者,读写均衡且要实时选后者。
Java并发容器的设计充满了“取舍哲学”:CopyOnWrite用空间换时间(牺牲内存换读性能),ConcurrentHashMap用算法优化(细粒度锁+CAS)平衡读写。理解它们的原理和适用场景,就能在多线程编程中得心应手~ 🚀
觉得有帮助的话,点赞收藏不迷路!下期聊聊“Java线程池:从Executors到自定义线程池,避坑指南”~