内容概要
CopyOnWriteArrayList
类的最大优点在于读取时无需加锁,非常适合读多写少的并发场景,由于其写操作通过复制底层数据来实现,从而保证了读取数据的一致性和高效性,此外,它简单易用,是快速实现线程安全列表的不错选择,CopyOnWriteArrayList在读操作占主导的场景下,能够提供出色的性能和稳定性。
核心概念
CopyOnWriteArrayList
类实现了 List
、RandomAccess
和Cloneable
接口,它是一个线程安全的变体,它的工作原理:当修改操作(如 add
、set
等)发生时,它会复制底层数组,然后在复制后的数组上进行修改,修改完成后再将内部的引用指向新的数组,这种设计使得读取操作可以在不进行任何锁定的情况下进行,因此非常适合读多写少的并发场景。
假设,有一个在线新闻发布系统,该系统维护了一个新闻列表,这个列表需要被频繁地读取(用户浏览新闻),但只偶尔被修改(发布新新闻或更新现有新闻),在这种情况下,使用 CopyOnWriteArrayList
来存储新闻列表可能是一个不错的选择。
- 读取操作:当用户浏览新闻时,系统需要从新闻列表中读取数据,由于
CopyOnWriteArrayList
的读取操作是无锁的,因此多个用户可以同时读取新闻列表而不会相互阻塞,这有助于提高系统的吞吐量。 - 写入操作:当新闻编辑发布新新闻或更新现有新闻时,系统需要修改新闻列表,虽然
CopyOnWriteArrayList
的写入操作会复制整个底层数组,但由于这种操作并不频繁,因此不会成为性能瓶颈,此外,由于写入操作是在新的数组上进行的,因此读取操作不会被阻塞,这有助于保持系统的响应性。
CopyOnWriteArrayList
类特别适合读多写少的场景中,它通常用来解决以下类似场景的问题:
- 线程安全:在多线程环境下,普通的
ArrayList
不是线程安全的,如果有多个线程同时修改ArrayList
,可能会导致数据不一致的问题,CopyOnWriteArrayList
通过内部的复制机制保证了线程安全,即当有线程对列表进行修改时(如添加、删除元素),它会先复制一份当前列表的副本,然后在副本上进行修改,修改完成后再将内部引用指向新的副本,这样读取操作就可以继续在原列表上进行,而不会被修改操作所阻塞。 - 读写分离:由于
CopyOnWriteArrayList
的写操作(修改操作)是在新的数组上进行的,而读操作则是在未修改的原始数组上进行,因此读写操作之间不会相互干扰,这种读写分离的设计使得CopyOnWriteArrayList
在高并发读取的场景下表现良好。 数据一致性:通过复制整个底层数组来确保修改操作的原子性,
CopyOnWriteArrayList
提供了强一致性保证,因此,读取操作要么看到的是修改之前的列表状态,要么看到的是修改之后的列表状态,而不会是中间状态。CopyOnWriteArrayList
并不适用于所有场景,由于其写操作需要复制整个底层数组,因此当列表很大或者写操作非常频繁时,它可能会导致较大的内存开销和性能下降。
代码案例
下面是一个简单的Java程序,演示了如何使用CopyOnWriteArrayList
类,案例中创建了一个CopyOnWriteArrayList
实例,并模拟了多个线程同时对其进行读写操作的情况,如下代码:
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
public class CopyOnWriteArrayListDemo {
public static void main(String[] args) {
// 创建一个CopyOnWriteArrayList实例
List<String> list = new CopyOnWriteArrayList<>();
// 启动一个线程向列表中添加元素
new Thread(() -> {
for (int i = 0; i < 5; i++) {
list.add("Item " + i);
try {
// 模拟耗时操作,让其他线程有机会读取数据
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("Writer Thread Finished!");
}).start();
// 启动多个线程从列表中读取元素
for (int i = 0; i < 3; i++) {
new Thread(() -> {
for (int j = 0; j < 10; j++) {
// 读取列表中的元素
System.out.println("Reader Thread " + Thread.currentThread().getId() + " is reading: " + list);
try {
// 模拟耗时操作
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
}
}
}
在上面代码中,创建了一个CopyOnWriteArrayList
的实例list
,启动了一个线程来模拟向列表中添加元素的写操作,这个线程使用循环向列表中添加5个元素,并在每次添加后暂停100毫秒以模拟耗时操作,接着,启动了3个线程来模拟从列表中读取元素的读操作,每个线程都使用循环读取列表10次,并在每次读取后暂停50毫秒以模拟耗时操作,由于CopyOnWriteArrayList
的写操作会复制底层数组,并且在新的数组上进行修改,因此读取操作不会受到写操作的影响,从而保证了线程安全和数据一致性。
核心API
CopyOnWriteArrayList
类提供了一个线程安全的 ArrayList
实现,它通过在修改时复制底层数组来实现线程安全,因此它特别适合于读多写少的并发场景,下面是CopyOnWriteArrayList` 类中一些主要方法的含义:
1、构造方法
CopyOnWriteArrayList()
: 创建一个空的CopyOnWriteArrayList
。CopyOnWriteArrayList(Collection<? extends E> c)
: 创建一个包含指定集合中所有元素的CopyOnWriteArrayList
。
2、读取方法(通常不会阻塞,因为读取时不需要锁定)
get(int index)
: 获取指定索引位置的元素。iterator()
: 返回一个迭代器,用于遍历列表中的元素。listIterator()
: 返回一个列表迭代器,允许以任何方向遍历列表,并修改元素(尽管在CopyOnWriteArrayList
中修改元素实际上会抛出UnsupportedOperationException
)。toArray()
: 返回一个包含列表中所有元素的数组。
3、修改方法(在修改时因为会复制底层数组,所以可能需要更多时间)
add(E e)
: 在列表的末尾添加一个元素。add(int index, E element)
: 在列表的指定位置插入一个元素。addAll(Collection<? extends E> c)
: 将指定集合中的所有元素添加到列表的末尾。addAll(int index, Collection<? extends E> c)
: 将指定集合中的所有元素插入到列表的指定位置。set(int index, E element)
: 替换列表中指定位置的元素。remove(int index)
: 移除列表中指定位置的元素。remove(Object o)
: 移除列表中第一个出现的指定元素(如果存在)。removeAll(Collection<?> c)
: 移除列表中所有包含在指定集合中的元素。retainAll(Collection<?> c)
: 仅在列表中保留包含在指定集合中的元素。clear()
: 清空列表中的所有元素。
4、查询方法
contains(Object o)
: 检查列表中是否包含指定元素。containsAll(Collection<?> c)
: 检查列表是否包含指定集合中的所有元素。isEmpty()
: 检查列表是否为空。size()
: 返回列表中的元素数量。indexOf(Object o)
: 返回列表中第一次出现指定元素的索引,如果列表不包含此元素,则返回 -1。lastIndexOf(Object o)
: 返回列表中最后一次出现指定元素的索引,如果列表不包含此元素,则返回 -1。
5、其他方法
subList(int fromIndex, int toIndex)
: 返回列表中指定的部分视图(注意,返回的不是新的独立列表,对返回的列表的修改会影响原列表,但由于CopyOnWriteArrayList
的特性,修改操作会抛出UnsupportedOperationException
)。equals(Object o)
: 比较此列表与指定对象是否相等。hashCode()
: 返回列表的哈希码值。CopyOnWriteArrayList
的写操作(修改方法)是通过复制底层数组来实现的,因此在高并发写入的场景下,性能可能会受到严重影响,这种情况下,其他并发数据结构,如ConcurrentHashMap
中的Segment
数组、ConcurrentLinkedQueue
或ConcurrentSkipListMap
等,可能会是更好的选择。但是,在读多写少的场景中,推荐使用CopyOnWriteArrayList
,它提供了一个高效且线程安全的解决方案。
核心总结
CopyOnWriteArrayList
类是一个线程安全的列表实现,非常适合读多写少的并发场景,优点在于读取操作无需锁定,性能高效,且能保证数据的一致性;写操作时,通过复制底层数组来避免阻塞读操作,从而实现读写分离。但是,它的写操作代价较高,因为每次修改都需要复制整个数组,这在数据量大或写操作频繁时会造成明显的性能开销,此外,它不适合需要频繁修改列表的场景,因为每次修改都会生成新的数组副本,导致内存占用较高。
在读取操作远多于写入操作,且对数据实时性要求不高的场景中,CopyOnWriteArrayList
是个不错的选择,但如果写操作频繁或数据量巨大,建议考虑其他更适合的并发数据结构,如ConcurrentLinkedQueue
或ConcurrentHashMap
等。
END!
END!
END!
往期回顾
精品文章
Java并发基础:ConcurrentSkipListMap全面解析
Java并发基础:ConcurrentSkipListSet全面解析!
Java并发基础:SynchronousQueue全面解析!
Java并发基础:ConcurrentLinkedQueue全面解析!