首先,CopyOnWriteArrayList是Java并发包中的一个线程安全的List实现,适用于读多写少的场景。
它的核心思想是写时复制,也就是在修改列表时,会复制一个新的数组,修改完成后再替换原来的数组。
这样读操作不需要加锁,可以保证高并发读的性能。
接下来,源码结构。比如,内部使用了一个volatile数组来存储数据,
写操作时通过ReentrantLock保证线程安全,然后复制数组进行修改。
需要详细解释关键方法的实现,比如add、remove、iterator等。比如,add方法会加锁
,复制数组,添加元素,然后设置新数组。迭代器是直接基于当前数组的快照,不会反映后续的修改。
然后是优缺点。优点肯定是读操作的高性能,无锁,线程安全。
缺点就是写操作开销大,每次修改都要复制整个数组,内存占用高,数据实时性差,
因为读的是旧数组的快照。所以适用于读多写少,比如监听器列表、配置信息的存储等场景。
用户可能是在设计一个高并发的系统,需要选择合适的集合类。可能遇到的困惑是,
在需要频繁写入的情况下使用CopyOnWriteArrayList会导致性能问题。
所以需要强调适用场景,避免误用。另外,用户可能对迭代器的行为不太清楚,
比如迭代过程中不会抛出ConcurrentModificationException,但也不会看到最新的修改。
和Collections.synchronizedList的区别,
或者与ConcurrentLinkedQueue等其他并发容器的比较。
总结回答结构:先介绍类的作用,然后源码解析,优缺点,使用场景,最后可能的最佳实践或者注意事项。
CopyOnWriteArrayList 是 Java 并发包(java.util.concurrent
)中提供的线程安全的 List
实现,其核心思想是 “写时复制”。以下是其源码分析、优缺点及典型使用场景。
一、源码关键实现
1. 核心数据结构
public class CopyOnWriteArrayList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable {
private static final long serialVersionUID = 8673264195747942595L;
// 使用ReentrantLock实现独占锁,确保写操作的原子性
final transient ReentrantLock lock = new ReentrantLock();
// 使用volatile修饰,确保多线程环境下对array的可见性
private transient volatile Object[] array;
// 获取array的引用
final Object[] getArray() {
return array;
}
// 设置array的引用
final void setArray(Object[] a) {
array = a;
}
// 无参构造器,初始化为一个空数组
public CopyOnWriteArrayList() {
setArray(new Object[0]);
}
// 接收一个Collection作为参数,将其元素复制到本List中
public CopyOnWriteArrayList(Collection<? extends E> c) {
Object[] elements;
if (c.getClass() == CopyOnWriteArrayList.class) {
elements = ((CopyOnWriteArrayList<?>) c).getArray();
} else {
elements = c.toArray();
if (elements.getClass() != Object[].class) {
elements = Arrays.copyOf(elements, elements.length, Object[].class);
}
}
setArray(elements);
}
// 接收一个数组作为参数,将其元素复制到本List中
public CopyOnWriteArrayList(E[] toCopyIn) {
setArray(Arrays.copyOf(toCopyIn, toCopyIn.length, Object[].class));
}
}
2. 写操作(add/remove/set)
以 add
方法为例:
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();
}
}
-
所有写操作均加锁,保证线程安全。
-
复制数组:每次修改都会创建一个新数组,替换旧数组。
3. 读操作(get/size/iterator)
public E get(int index) {
return get(getArray(), index); // 直接读取当前数组,无需加锁
}
private E get(Object[] a, int index) {
return (E) a[index];
}
// 迭代器基于当前数组的快照,不会感知后续修改
public Iterator<E> iterator() {
return new COWIterator<E>(getArray(), 0);
}
-
读操作无锁,直接访问当前数组。
-
弱一致性迭代器:迭代器遍历的是操作发生时的数组快照。
4. 删除操作(remove)
public E remove(int index) {
final ReentrantLock lock = this.lock;
lock.lock();
try {
Object[] elements = getArray();
int len = elements.length;
E oldValue = get(elements, index);
int numMoved = len - index - 1;
if (numMoved == 0) {
setArray(Arrays.copyOf(elements, len - 1));
} else {
Object[] newElements = new Object[len - 1];
System.arraycopy(elements, 0, newElements, 0, index);
System.arraycopy(elements, index + 1, newElements, index, numMoved);
setArray(newElements);
}
return oldValue;
} finally {
lock.unlock();
}
}
二、优缺点分析
优点
-
读性能极高
读操作无需加锁,适合读多写少的场景(如监听器列表、配置管理)。 -
线程安全
通过写时复制和锁机制保证线程安全,简化了并发编程的复杂性。 -
避免
ConcurrentModificationException
提供了一个弱一致性的迭代器,在迭代过程中,即使列表发生了修改,迭代器仍然可以安全地进行,不会抛出ConcurrentModificationException异常。(迭代器基于快照,遍历时即使发生写操作也不会抛出异常。) - 适合读多写少的场景:由于读操作无锁且非常高效,适用于读操作频繁而写操作较少的场景。
缺点
-
内存开销大
由于每次写操作都会创建底层数组的一个副本,因此内存开销相对较大。特别是在列表包含大量元素时,每次写操作都需要复制整个数组,这不仅会增加内存消耗,还可能导致垃圾回收频繁发生,影响系统性能。 -
写性能差: 由于每次写操作都需要复制整个底层数组,时间复杂度是O(n),其中n是数组的大小。因此,在需要频繁写操作的场景下,性能会非常低下。
-
数据实时性差
由于写操作会复制整个数组,因此在读取过程中,如果另一个线程完成了写操作,但由于读操作是在旧数组上进行的,读取线程依然会获取到写操作之前的数据(读操作可能读到旧数据,无法保证强一致性)。 -
不适合频繁修改的场景
大量写操作会导致内存和性能问题(如频繁更新的缓存)。
三、使用场景
1. 监听器列表(Listener Lists): 在事件监听器的管理中,当事件发生时需要遍历监听器,使用CopyOnWriteArrayList可以确保所有的监听器都能够被一致地访问。
// 典型应用:事件监听器管理
public class EventDispatcher {
private final CopyOnWriteArrayList<EventListener> listeners =
new CopyOnWriteArrayList<>();
public void addListener(EventListener listener) {
listeners.add(listener);
}
public void fireEvent(Event event) {
// 遍历时无需担心并发修改
for (EventListener listener : listeners) {
listener.onEvent(event);
}
}
}
-
场景特点:监听器的注册(写)较少,事件触发(遍历读)频繁。
2. 配置管理 : 存储一些系统的全局配置项,这些配置项很少变化,但需要频繁读取。例如,Web应用中需要频繁读取但很少修改的配置列表
// 全局配置信息存储
public class AppConfig {
private static CopyOnWriteArrayList<String> configEntries =
new CopyOnWriteArrayList<>();
public static void reloadConfig() {
List<String> newConfig = loadConfigFromFile();
configEntries.clear();
configEntries.addAll(newConfig); // 全量替换配置
}
public static String getConfig(int index) {
return configEntries.get(index);
}
}
-
场景特点:配置更新频率低,读取频率高。
3. 黑名单/白名单
-
名单变动较少,但需要高并发查询。
3. 缓存 :
数据缓存中可能有频繁的读取,但更新相对较少,此时CopyOnWriteArrayList可以确保读取的高效性。
4. 日志记录 :
多线程环境中遍历需要输出到日志的数据时,使用CopyOnWriteArrayList可以避免并发修改带来的不一致性。
5.需要快照的场景
在多线程环境中,应用程序可能需要对集合进行快照操作,CopyOnWriteArrayList由于其天然的快照机制,非常适合这种需求
四、与其他线程安全容器的对比
容器 | 适用场景 | 锁机制 | 一致性 |
---|---|---|---|
CopyOnWriteArrayList | 读多写少(如监听器列表) | 写时复制 + ReentrantLock | 弱一致性(快照) |
Collections.synchronizedList | 读写均衡 | 全表锁 | 强一致性 |
ConcurrentLinkedQueue | 高并发队列(生产者-消费者) | CAS 无锁 | 弱一致性 |
五、最佳实践
-
避免频繁写操作
若需频繁修改,考虑使用ConcurrentLinkedQueue
或ConcurrentHashMap
。 -
控制数组大小
存储大量数据时,频繁复制可能导致内存压力。 -
明确弱一致性的影响
迭代器无法感知后续修改,需根据业务需求选择。
总结
CopyOnWriteArrayList 通过 写时复制 实现了读操作的无锁和高性能,但牺牲了写操作的效率和数据实时性。其核心价值在于 读多写少的高并发场景,开发者需根据业务特点权衡选择。
学海无涯,志当存远。燃心砺志,奋进不辍。愿诸君得此鸡汤,如沐春风,学业有成。若觉此言甚善,烦请赐赞一枚,共励学途,同铸辉煌