CopyOnWriteArrayList
CopyOnWriteArrayList
是 Java 并发包中的一个线程安全的 List 实现,适用于读多写少的场景。它通过在每次修改(如添加、删除元素)时复制整个底层数组,确保并发环境下的线程安全性。
主要特点
-
线程安全:
- 通过复制整个数组来确保线程安全,所有的修改操作(添加、删除、设置等)都会创建一个新的数组。
-
读操作无锁:
- 读操作不需要加锁,直接访问底层数组,因此读操作非常快,可以并发进行。
-
写操作开销大:
- 每次写操作都会复制整个数组,这使得写操作的开销比较大,适用于写操作相对较少的场景。
写操作的复制机制
每次执行写操作时,CopyOnWriteArrayList
会进行以下步骤:
- 获取当前数组的副本。
- 在副本上执行修改操作。
- 将修改后的副本设置为新的底层数组。
这种机制保证了写操作不会影响正在进行的读操作,从而确保了线程安全。
使用场景
CopyOnWriteArrayList
适用于以下场景:
-
读多写少:
- 由于写操作的开销较大,而读操作的性能非常好,因此非常适合读多写少的应用场景。
-
迭代器稳定性要求高:
- 由于迭代器是基于底层数组的快照创建的,因此在迭代过程中,如果有其他线程进行了修改操作,迭代器仍然可以安全地遍历原来的数据,不会抛出
ConcurrentModificationException
。
- 由于迭代器是基于底层数组的快照创建的,因此在迭代过程中,如果有其他线程进行了修改操作,迭代器仍然可以安全地遍历原来的数据,不会抛出
示例代码
import java.util.concurrent.CopyOnWriteArrayList;
public class Main {
public static void main(String[] args) {
CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();
// 添加元素
list.add("A");
list.add("B");
list.add("C");
// 读操作
for (String s : list) {
System.out.println(s);
}
// 写操作
list.add("D");
// 再次读操作
for (String s : list) {
System.out.println(s);
}
}
}
性能分析
- 读操作性能:由于读操作不需要加锁,因此性能非常高,适合并发读取场景。
- 写操作性能:写操作需要复制整个数组,因此当列表较大且写操作频繁时,性能会受到影响。
注意事项
-
内存消耗:
- 每次写操作都会创建新的数组,因此会有额外的内存消耗,特别是在大数组频繁写入的情况下。
-
写操作性能:
- 由于写操作的开销较大,因此在写操作频繁的场景中,
CopyOnWriteArrayList
的性能可能不如其他线程安全的集合类。
- 由于写操作的开销较大,因此在写操作频繁的场景中,
线程安全的实现
CopyOnWriteArrayList
通过以下几个关键步骤来实现线程安全:
-
不可变性:
CopyOnWriteArrayList
内部存储元素的数组是不可变的。每次写操作(如添加、删除元素)都会创建一个新的数组副本,而不会修改原有数组。这确保了在读操作进行时,底层数组不会被修改,从而避免了并发问题。 -
原子性:
CopyOnWriteArrayList
使用volatile
关键字来确保对底层数组的引用是线程可见的,并使用synchronized
块来确保写操作的原子性。这样可以确保在任何时候,只有一个线程可以进行写操作,从而避免并发修改的问题。
确保只有一个副本
当进行写操作时,CopyOnWriteArrayList
会创建一个新的数组副本,并将这个副本设置为内部的数组引用。通过 volatile
关键字来保证新数组对其他线程可见,从而确保读操作始终使用最新的数组副本。
示例代码分析
以下是 CopyOnWriteArrayList
的部分源码,用于展示其线程安全和副本机制:
import java.util.Arrays;
import java.util.concurrent.locks.ReentrantLock;
public class CopyOnWriteArrayList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable {
private transient volatile Object[] array;
final transient ReentrantLock lock = new ReentrantLock();
public CopyOnWriteArrayList() {
setArray(new Object[0]);
}
final Object[] getArray() {
return array;
}
final void setArray(Object[] a) {
array = a;
}
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 boolean remove(Object o) {
final ReentrantLock lock = this.lock;
lock.lock();
try {
Object[] elements = getArray();
int len = elements.length;
if (len == 0) {
return false;
}
int newlen = len - 1;
Object[] newElements = new Object[newlen];
for (int i = 0; i < len; i++) {
if (o.equals(elements[i])) {
System.arraycopy(elements, 0, newElements, 0, i);
System.arraycopy(elements, i + 1, newElements, i, newlen - i);
setArray(newElements);
return true;
}
}
return false;
} finally {
lock.unlock();
}
}
// 其他方法省略
}
解释
-
数组副本 (
copy
):在add
和remove
方法中,当需要进行写操作时,会首先获取当前数组 (elements
),然后创建一个新数组 (newElements
),并将现有数组的内容复制到新数组中。对于add
操作,新数组的长度增加一,并将新元素添加到新数组中。对于remove
操作,新数组的长度减少一,并从中删除指定元素。 -
锁机制 (
ReentrantLock
):写操作使用ReentrantLock
来确保原子性。锁的使用确保在任何时候,只有一个线程可以进行写操作,其他线程必须等待锁释放后才能进行写操作。这保证了写操作的线程安全性。 -
volatile
关键字:数组引用 (array
) 使用volatile
关键字,这确保了当写操作完成后,新数组对所有读取线程立即可见。读操作始终访问当前的数组引用,因此不会受到写操作的影响。
CopyOnWriteArrayList in Spring
CopyOnWriteArrayList
在 Spring 框架中也有一些典型的应用场景,特别是在事件监听和处理器管理等需要高并发读写的地方。一个常见的例子是在 Spring 的事件发布机制中,用于管理事件监听器列表,以确保在高并发环境下的线程安全。
应用场景
Spring 的事件机制允许应用程序在事件发生时发布事件,其他组件可以监听这些事件并作出响应。CopyOnWriteArrayList
常用于管理这些事件监听器,因为它可以确保在高并发环境下的安全性,允许多个线程同时读取监听器列表而不会被阻塞。
代码示例
以下是一个使用 CopyOnWriteArrayList
来管理 Spring 事件监听器的示例代码,展示了如何确保高并发下的线程安全。
场景描述
我们创建一个自定义的事件发布者和事件监听器,使用 CopyOnWriteArrayList
来管理监听器列表。当事件发布时,所有监听器都会被通知。
示例代码
首先,我们定义一个自定义事件:
import org.springframework.context.ApplicationEvent;
public class CustomEvent extends ApplicationEvent {
private final String message;
public CustomEvent(Object source, String message) {
super(source);
this.message = message;
}
public String getMessage() {
return message;
}
}
接下来,定义一个事件监听器接口:
public interface CustomEventListener {
void onCustomEvent(CustomEvent event);
}
然后,我们创建一个事件发布者类,使用 CopyOnWriteArrayList
来管理监听器:
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
public class CustomEventPublisher {
private final List<CustomEventListener> listeners = new CopyOnWriteArrayList<>();
public void addListener(CustomEventListener listener) {
listeners.add(listener);
}
public void removeListener(CustomEventListener listener) {
listeners.remove(listener);
}
public void publishEvent(String message) {
CustomEvent event = new CustomEvent(this, message);
for (CustomEventListener listener : listeners) {
listener.onCustomEvent(event);
}
}
}
最后,创建一个示例应用程序,展示如何使用事件发布者和监听器:
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class CustomEventApp {
public static void main(String[] args) {
ApplicationContext context = new AnnotationConfigApplicationContext(CustomEventApp.class);
CustomEventPublisher publisher = context.getBean(CustomEventPublisher.class);
CustomEventListener listener1 = event -> System.out.println("Listener1 received: " + event.getMessage());
CustomEventListener listener2 = event -> System.out.println("Listener2 received: " + event.getMessage());
publisher.addListener(listener1);
publisher.addListener(listener2);
publisher.publishEvent("Hello, World!");
publisher.publishEvent("Spring Events are cool!");
}
@Bean
public CustomEventPublisher customEventPublisher() {
return new CustomEventPublisher();
}
}
解释
- 自定义事件 (
CustomEvent
):继承自ApplicationEvent
,用于携带事件信息。 - 事件监听器接口 (
CustomEventListener
):定义一个方法onCustomEvent
来处理接收到的事件。 - 事件发布者 (
CustomEventPublisher
):使用CopyOnWriteArrayList
来管理监听器列表,提供添加和移除监听器的方法,并在发布事件时通知所有监听器。 - 示例应用程序 (
CustomEventApp
):配置 Spring 上下文,创建事件发布者和监听器,并演示事件发布和处理过程。
ConcurrentSkipListMap
ConcurrentSkipListMap 和 ConcurrentSkipListSet 分别是基于跳表(Skip List)数据结构实现的并发版本的 TreeMap 和 TreeSet。
ConcurrentSkipListMap
是 Java 并发包 (java.util.concurrent
) 中的一个线程安全且有序的 Map 实现。它基于跳表 (SkipList) 数据结构,提供高效的并发访问。
主要特点
-
线程安全:
ConcurrentSkipListMap
是线程安全的,可以在并发环境中高效地进行读取和写入操作。
-
有序性:
- 元素按键的自然顺序或自定义比较器的顺序排列,支持高效的范围操作和按顺序的遍历。
-
非阻塞算法:
- 使用了无锁的算法 (CAS 操作),避免了传统锁的开销,提升了并发性能。
跳表数据结构
跳表是一种平衡数据结构,具有多层链表,每一层链表都是底层链表的子集。跳表通过多层级链表来实现快速查找、插入和删除操作。
使用场景
ConcurrentSkipListMap
适用于以下场景:
-
高并发环境下的有序映射:
- 需要高效地在并发环境中进行插入、删除和查找操作,同时保持键的顺序。
-
范围查询:
- 需要进行范围查询(如获取某个键范围内的所有键值对),利用有序特性可以高效地实现。
-
优先级队列:
- 可以用作优先级队列,支持按优先级顺序处理任务。
示例代码
import java.util.concurrent.ConcurrentSkipListMap;
public class Main {
public static void main(String[] args) {
ConcurrentSkipListMap<String, Integer> map = new ConcurrentSkipListMap<>();
// 插入元素
map.put("apple", 10);
map.put("banana", 20);
map.put("cherry", 30);
// 读取元素
System.out.println("apple: " + map.get("apple"));
// 遍历元素
for (String key : map.keySet()) {
System.out.println(key + ": " + map.get(key));
}
// 范围查询
System.out.println("Keys from apple to cherry: " + map.subMap("apple", "cherry"));
}
}
性能分析
- 读写性能:
ConcurrentSkipListMap
使用无锁算法进行并发访问,读写性能高效,适合高并发场景。 - 有序性开销:由于保持有序性,写操作(插入和删除)开销略高于无序的
ConcurrentHashMap
。
注意事项
-
内存消耗:
- 跳表的多层结构会占用额外的内存,特别是在存储大量元素时,需要注意内存使用情况。
-
写操作开销:
- 相对于无序的并发集合(如
ConcurrentHashMap
),有序集合的写操作(插入、删除)开销较大,因为需要维护顺序。
- 相对于无序的并发集合(如
总结
ConcurrentSkipListMap
是在高并发环境中需要有序映射的理想选择,结合了跳表的数据结构和无锁算法,提供了高效的读写性能和良好的有序性。适用于需要频繁进行范围查询或优先级处理的场景。