Java中的CopyOnWrite容器

本文详细介绍了Java并发包中CopyOnWrite容器的实现原理及应用场景,包括CopyOnWriteArrayList和CopyOnWriteMap的使用方式、优缺点以及如何在不同场景中有效利用这些容器来提高并发性能。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

转自酷壳   http://coolshell.cn/articles/11175.html

Copy-On-Write简称COW,是一种用于程序设计中的优化策略。其基本思路是,从一开始大家都在共享同一个内容,当某个人想要修改这个内容的时候,才会真正把内容Copy出去形成一个新的内容然后再改,这是一种延时懒惰策略。从JDK1.5开始Java并发包里提供了两个使用CopyOnWrite机制实现的并发容器,它们是CopyOnWriteArrayList和CopyOnWriteArraySet。CopyOnWrite容器非常有用,可以在非常多的并发场景中使用到。

什么是CopyOnWrite容器

CopyOnWrite容器即写时复制的容器。通俗的理解是当我们往一个容器添加元素的时候,不直接往当前容器添加,而是先将当前容器进行Copy,复制出一个新的容器,然后新的容器里添加元素,添加完元素之后,再将原容器的引用指向新的容器。这样做的好处是我们可以对CopyOnWrite容器进行并发的读,而不需要加锁,因为当前容器不会添加任何元素。所以CopyOnWrite容器也是一种读写分离的思想,读和写不同的容器。

CopyOnWriteArrayList的实现原理

在使用CopyOnWriteArrayList之前,我们先阅读其源码了解下它是如何实现的。以下代码是向ArrayList里添加元素,可以发现在添加的时候是需要加锁的,否则多线程写的时候会Copy出N个副本出来。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public boolean add(T 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();
 
     }
 
}
 
final void setArray(Object[] a) {
     array = a;
}

读的时候不需要加锁,如果读的时候有多个线程正在向ArrayList添加数据,读还是会读到旧的数据,因为写的时候不会锁住旧的ArrayList。

1
2
3
public E get( int index) {
     return get(getArray(), index);
}

JDK中并没有提供CopyOnWriteMap,我们可以参考CopyOnWriteArrayList来实现一个,基本代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import java.util.Collection;
import java.util.Map;
import java.util.Set;
 
public class CopyOnWriteMap<K, V> implements Map<K, V>, Cloneable {
     private volatile Map<K, V> internalMap;
 
     public CopyOnWriteMap() {
         internalMap = new HashMap<K, V>();
     }
 
     public V put(K key, V value) {
 
         synchronized ( this ) {
             Map<K, V> newMap = new HashMap<K, V>(internalMap);
             V val = newMap.put(key, value);
             internalMap = newMap;
             return val;
         }
     }
 
     public V get(Object key) {
         return internalMap.get(key);
     }
 
     public void putAll(Map<? extends K, ? extends V> newData) {
         synchronized ( this ) {
             Map<K, V> newMap = new HashMap<K, V>(internalMap);
             newMap.putAll(newData);
             internalMap = newMap;
         }
     }
}

实现很简单,只要了解了CopyOnWrite机制,我们可以实现各种CopyOnWrite容器,并且在不同的应用场景中使用。

CopyOnWrite的应用场景

CopyOnWrite并发容器用于读多写少的并发场景。比如白名单,黑名单,商品类目的访问和更新场景,假如我们有一个搜索网站,用户在这个网站的搜索框中,输入关键字搜索内容,但是某些关键字不允许被搜索。这些不能被搜索的关键字会被放在一个黑名单当中,黑名单每天晚上更新一次。当用户搜索时,会检查当前关键字在不在黑名单当中,如果在,则提示不能搜索。实现代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
package com.ifeve.book;
 
import java.util.Map;
 
import com.ifeve.book.forkjoin.CopyOnWriteMap;
 
/**
  * 黑名单服务
  *
  * @author fangtengfei
  *
  */
public class BlackListServiceImpl {
 
     private static CopyOnWriteMap<String, Boolean> blackListMap = new CopyOnWriteMap<String, Boolean>(
             1000 );
 
     public static boolean isBlackList(String id) {
         return blackListMap.get(id) == null ? false : true ;
     }
 
     public static void addBlackList(String id) {
         blackListMap.put(id, Boolean.TRUE);
     }
 
     /**
      * 批量添加黑名单
      *
      * @param ids
      */
     public static void addBlackList(Map<String,Boolean> ids) {
         blackListMap.putAll(ids);
     }
 
}

代码很简单,但是使用CopyOnWriteMap需要注意两件事情:

1. 减少扩容开销。根据实际需要,初始化CopyOnWriteMap的大小,避免写时CopyOnWriteMap扩容的开销。

2. 使用批量添加。因为每次添加,容器每次都会进行复制,所以减少添加次数,可以减少容器的复制次数。如使用上面代码里的addBlackList方法。

CopyOnWrite的缺点

CopyOnWrite容器有很多优点,但是同时也存在两个问题,即内存占用问题和数据一致性问题。所以在开发的时候需要注意一下。

内存占用问题。因为CopyOnWrite的写时复制机制,所以在进行写操作的时候,内存里会同时驻扎两个对象的内存,旧的对象和新写入的对象(注意:在复制的时候只是复制容器里的引用,只是在写的时候会创建新对象添加到新容器里,而旧容器的对象还在使用,所以有两份对象内存)。如果这些对象占用的内存比较大,比如说200M左右,那么再写入100M数据进去,内存就会占用300M,那么这个时候很有可能造成频繁的Yong GC和Full GC。之前我们系统中使用了一个服务由于每晚使用CopyOnWrite机制更新大对象,造成了每晚15秒的Full GC,应用响应时间也随之变长。

针对内存占用问题,可以通过压缩容器中的元素的方法来减少大对象的内存消耗,比如,如果元素全是10进制的数字,可以考虑把它压缩成36进制或64进制。或者不使用CopyOnWrite容器,而使用其他的并发容器,如ConcurrentHashMap

数据一致性问题。CopyOnWrite容器只能保证数据的最终一致性,不能保证数据的实时一致性。所以如果你希望写入的的数据,马上能读到,请不要使用CopyOnWrite容器。

关于C++的STL中,曾经也有过Copy-On-Write的玩法,参见陈皓的《C++ STL String类中的Copy-On-Write》,后来,因为有很多线程安全上的事,就被去掉了。


<think>我们正在讨论Java并发包中的并发容器。根据引用[1]和[2],Java并发包提供了一系列线程安全的容器,用于解决多线程环境下的共享数据问题。这些容器可以分为几类:线程安全的List、Set、Map以及队列(队列已在之前的问题中详细讨论,这里主要关注非队列的容器)。 分类如下: 1. List: - CopyOnWriteArrayList: 基于写时复制技术,适用于读多写少的场景。 2. Set: - CopyOnWriteArraySet: 基于CopyOnWriteArrayList实现,同样适用于读多写少的场景。 - ConcurrentSkipListSet: 基于跳表实现的有序集合,支持并发访问。 3. Map: - ConcurrentHashMap: 高并发环境下使用的HashMap,通过分段锁(JDK7)或CAS+synchronized(JDK8)实现线程安全。 - ConcurrentSkipListMap: 基于跳表实现的有序Map,支持并发访问。 4. 队列(Queue/Deque): - 上一问题已详细讨论,包括阻塞队列、非阻塞队列等,这里不再重复。 下面我们详细说明每个容器的特点和使用场景。 一、List容器 1. CopyOnWriteArrayList - 原理:写操作(add, set, remove等)时,会复制底层数组,修改在新数组上进行,最后将新数组替换旧数组。读操作无锁,直接访问当前数组。 - 特点:读操作非常快,写操作因为需要复制数组,所以开销较大。适合读多写少的场景。 - 注意:由于读操作不会阻塞,所以可能读到旧数据,因此适合对数据一致性要求不高的场景。 - 示例代码: ```java CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>(); // 写操作(会复制数组) list.add("item1"); // 读操作(无锁) String item = list.get(0); ``` 二、Set容器 1. CopyOnWriteArraySet - 原理:内部使用CopyOnWriteArrayList实现,因此特性与CopyOnWriteArrayList类似。 - 特点:同样适用于读多写少的场景,写操作开销大,读操作无锁。 - 注意:由于基于CopyOnWriteArrayList,所以它不适用于元素数量很大的集合,因为每次写操作都要复制整个数组。 2. ConcurrentSkipListSet - 原理:基于ConcurrentSkipListMap实现,使用跳表数据结构,保证元素的有序性(自然顺序或通过Comparator)。 - 特点:支持并发的插入、删除和查找操作,且操作的时间复杂度为$O(\log n)$。 - 适用场景:需要有序且并发的集合。 三、Map容器 1. ConcurrentHashMap - 原理:在JDK7中,使用分段锁(Segment)来减小锁的粒度,每个段独立加锁。在JDK8中,改为使用CAS+synchronized来保证并发安全,并且当链表长度超过阈值时转换为红黑树,以提高查找效率。 - 特点:高并发环境下性能优异,比Hashtable和Collections.synchronizedMap()性能好很多。 - 适用场景:高并发且需要键值对存储的场景。 - 示例代码: ```java ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>(); map.put("key1", 1); int value = map.get("key1"); ``` 2. ConcurrentSkipListMap - 原理:基于跳表(Skip List)实现的有序Map。跳表是一种可以快速查找、插入、删除的数据结构,通过多级索引实现。 - 特点:支持高并发的有序Map,所有操作都是线程安全的,且操作的时间复杂度为$O(\log n)$。 - 适用场景:需要有序且并发的键值对存储,例如排行榜等。 四、队列容器(简要提及,因为之前问题已覆盖) - 包括BlockingQueue的实现(如ArrayBlockingQueue, LinkedBlockingQueue, PriorityBlockingQueue等)和非阻塞队列(如ConcurrentLinkedQueue, ConcurrentLinkedDeque)等。 总结表格: | 容器类型 | 实现类 | 特点 | 适用场景 | |---------|--------|------|---------| | List | CopyOnWriteArrayList | 读无锁,写复制数组 | 读多写少,如监听器列表 | | Set | CopyOnWriteArraySet | 基于CopyOnWriteArrayList,读多写少 | 读多写少的集合 | | Set | ConcurrentSkipListSet | 基于跳表,有序,并发安全 | 需要有序的并发集合 | | Map | ConcurrentHashMap | 分段锁或CAS+synchronized,高并发 | 高并发键值存储,如缓存 | | Map | ConcurrentSkipListMap | 跳表实现,有序 | 需要有序的并发键值存储 | 注意:使用这些并发容器时,需要根据具体场景选择。例如,如果对读性能要求极高且写操作很少,可以选择CopyOnWrite系列;如果对并发写要求高,则选择ConcurrentHashMap或ConcurrentSkipListMap。 常见问题: 1. 如何选择List容器? - 如果需要频繁修改(写操作),则不建议使用CopyOnWriteArrayList,因为写操作会复制整个数组。此时,可以考虑使用Collections.synchronizedList(),但要注意其性能不如CopyOnWriteArrayList在读多写少场景下的表现。 2. ConcurrentHashMap在JDK7和JDK8中的区别? - JDK7:使用分段锁(Segment),每个段相当于一个小的HashMap,锁的粒度是段。 - JDK8:使用Node数组+链表+红黑树,锁的粒度是每个数组元素(桶),使用CAS和synchronized来保证并发安全。当链表长度大于8且数组长度超过64时,链表转换为红黑树。 3. 为什么需要ConcurrentSkipListMap? - 因为ConcurrentHashMap是无序的,当需要有序的Map时,可以使用ConcurrentSkipListMap。它提供了并发环境下的有序映射。 示例代码:ConcurrentHashMap的线程安全操作 ```java ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>(); // 多个线程可以安全地执行put操作 new Thread(() -> map.put("key2", 2)).start(); new Thread(() -> map.put("key3", 3)).start(); // 使用原子更新方法 map.computeIfAbsent("key4", k -> 4); ``` 最后,注意并发容器并不能保证所有复合操作都是原子性的,例如: ```java // 非原子操作 if (!map.containsKey("key")) { map.put("key", 1); } // 应该使用原子方法 map.putIfAbsent("key", 1); ``` 因此,使用并发容器时,应尽量使用其提供的原子方法(如putIfAbsent, compute, computeIfAbsent, merge等)来避免竞态条件。 相关问题: 1. CopyOnWriteArrayList的写操作是如何保证线程安全的? 2. ConcurrentHashMap在JDK8中是如何实现扩容的? 3. 跳表(Skip List)的数据结构是怎样的?为什么它适合并发? 4. 并发容器和普通容器通过Collections.synchronizedXXX包装的容器有什么区别?</think>### Java 并发包中的并发容器详解 Java 并发包(`java.util.concurrent`)提供了多种线程安全的容器类,用于解决多线程环境下的数据共享问题。这些容器通过高效的同步机制(如 CAS、写时复制、分段锁等)平衡线程安全与性能。以下是主要分类及实现: --- #### 一、线程安全 List | 实现类 | 核心机制 | 特点 | 适用场景 | |-------------------------|----------------------|----------------------------------------------------------------------|------------------------------| | **`CopyOnWriteArrayList`** | 写时复制 (Copy-on-Write) | 写操作复制新数组,读操作无锁;读多写少时性能高,写操作开销大[^1] | 监听器列表、配置管理等低频写场景 | | **`Collections.synchronizedList`** | 互斥锁 | 包装普通`ArrayList`,所有方法用`synchronized`同步;简单但吞吐量低 | 兼容旧代码的简单同步需求 | **示例**: ```java // 高并发读场景 List<String> safeList = new CopyOnWriteArrayList<>(); safeList.add("data"); // 写时复制 String data = safeList.get(0); // 无锁读 // 简单同步 List<String> syncList = Collections.synchronizedList(new ArrayList<>()); ``` --- #### 二、线程安全 Set | 实现类 | 核心机制 | 特点 | 适用场景 | |-------------------------|----------------------|----------------------------------------------------------------------|------------------------------| | **`CopyOnWriteArraySet`** | 写时复制 | 基于`CopyOnWriteArrayList`实现,特性类似[^1] | 读多写少的集合操作 | | **`ConcurrentSkipListSet`** | 跳表 (Skip List) | 有序集合(自然序或`Comparator`),CAS无锁实现,高并发插入/删除 | 需排序的高并发集合(如排行榜)| | **`Collections.synchronizedSet`** | 互斥锁 | 包装普通`HashSet`,方法全同步 | 简单同步需求 | **示例**: ```java Set<Integer> leaderboard = new ConcurrentSkipListSet<>(); leaderboard.add(100); // 并发插入有序集合 ``` --- #### 三、线程安全 Map | 实现类 | 核心机制 | 特点 | 适用场景 | |-------------------------|----------------------|----------------------------------------------------------------------|------------------------------| | **`ConcurrentHashMap`** | 分段锁 + CAS (JDK7)<br>CAS + `synchronized` (JDK8+) | 高并发读写,锁粒度细;JDK8 引入红黑树优化哈希冲突 | 缓存、会话存储等高并发 K-V 场景 | | **`ConcurrentSkipListMap`** | 跳表 | 有序 Map,支持并发排序和范围查询 | 需排序的并发字典(如范围检索) | | **`Collections.synchronizedMap`** | 互斥锁 | 包装普通`HashMap`,全方法同步 | 简单同步需求 | **`ConcurrentHashMap` 关键优化**: - JDK8 取消分段锁,改用 `Node` + CAS + `synchronized` 锁单个桶(Bucket) - 哈希冲突时链表转红黑树(阈值:链表长度 ≥ 8) - 支持原子方法:`compute()`, `merge()`, `search()` 等[^1] **示例**: ```java ConcurrentHashMap<String, Integer> cache = new ConcurrentHashMap<>(); // 原子更新 cache.compute("key", (k, v) -> (v == null) ? 1 : v + 1); ``` --- #### 四、线程安全队列(Queue/Deque) > 详见上一问题总结,核心实现包括: > - **阻塞队列**:`ArrayBlockingQueue`, `LinkedBlockingQueue` > - **非阻塞队列**:`ConcurrentLinkedQueue`, `ConcurrentLinkedDeque` > - **特殊队列**:`DelayQueue`, `PriorityBlockingQueue`[^2][^3] --- ### 并发容器选择指南 | 需求场景 | 推荐容器 | |------------------------|-----------------------------------| | 高频读 + 低频写 | `CopyOnWriteArrayList`/`CopyOnWriteArraySet` | | 高并发 K-V 存储 | `ConcurrentHashMap` | | 有序集合 + 高并发 | `ConcurrentSkipListSet`/`ConcurrentSkipListMap` | | 生产者-消费者模型 | `ArrayBlockingQueue`(有界) | | 定时任务调度 | `DelayQueue` | --- ### 常见问题解决方案 #### ❌ 复合操作非原子性 ```java // 错误:contains + add 非原子 if (!set.contains(item)) { set.add(item); } // 正确:使用 ConcurrentHashMap 的原子方法 ConcurrentHashMap<String, Boolean> map = new ConcurrentHashMap<>(); map.putIfAbsent("key", true); ``` #### ❌ `CopyOnWriteArrayList` 写性能差 ```java // 避免频繁写操作:批量添加 List<String> batchData = Arrays.asList("a", "b", "c"); safeList.addAll(batchData); // 减少复制次数 ``` #### ❌ 遍历时并发修改 ```java // ConcurrentHashMap 迭代器弱一致性(不抛异常) ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>(); map.forEach((k, v) -> process(k, v)); // 安全遍历 ``` --- ### 总结 Java 并发容器的核心设计思想: 1. **降低锁粒度**:如 `ConcurrentHashMap` 的分段锁/CAS 优化。 2. **读写分离**:如 `CopyOnWriteArrayList` 写时复制。 3. **无锁算法**:如 `ConcurrentSkipListMap` 的跳表+CAS。 4. **专用场景优化**:如 `DelayQueue` 的延迟调度。 > **关键原则**: > - 优先选择并发专用容器(如 `ConcurrentHashMap`),而非 `Collections.synchronizedXXX` 包装类。 > - 根据读写比例选择数据结构(如读多写少用 `CopyOnWriteArrayList`)。 > - 有界性设计防止资源耗尽(如队列显式设置容量)。 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值