内容概要
CopyOnWriteArraySet
类的优点在于能够实现无锁读取,确保高并发下的读取性能,同时,写操作通过复制底层数据来保证数据一致性,避免了多线程间的数据冲突,因此,它非常适合读多写少且对数据一致性要求较高的场景。
核心概念
假如,有一个在线购物平台,其中有一个功能是展示当前热门的商品,这个“热门商品”列表需要实时更新,因为用户的购买行为会不断改变商品的热门程度,同时,这个列表会被很多用户同时查看,因此它是一个读多写少的场景。
为了保证用户看到的热门商品列表总是最新的,每当有用户购买商品时,都需要更新这个列表,但是,由于更新操作可能会比较耗时(比如需要重新计算商品的热门度并排序),不希望这个更新过程阻塞用户查看列表的操作,此外,由于这个列表是实时更新的,也不能简单地锁定整个列表,因为这会导致用户看到的信息过时。
CopyOnWriteArraySet
就非常适合解决这类场景的问题,可以将热门商品存储在一个CopyOnWriteArraySet
中,每当有用户购买商品时,可以创建一个新的CopyOnWriteArraySet
对象,将更新后的商品列表复制进去,然后再将这个新对象替换掉原来的对象,由于这个替换操作是原子的,所以用户要么看到的是更新前的列表,要么看到的是更新后的列表,不会出现中间状态。
即使有多个用户同时查看热门商品列表,他们也不会被更新操作阻塞,而且,由于每次更新都会创建一个新的对象,所以用户看到的总是最新的信息(至少是在他们查看的那一瞬间是最新的),这就是CopyOnWriteArraySet
在处理读多写少、要求实时更新且不能阻塞读操作的场景中的典型应用。
如果写操作非常频繁,或者数据集非常大(因为每次更新都需要复制整个数据集),那么使用CopyOnWriteArraySet
可能会带来性能问题。
CopyOnWriteArraySet的实现原理
CopyOnWriteArraySet
类实现了Set接口,内部使用了一个CopyOnWriteArrayList
来存储元素,当对集合进行修改(如add、remove等操作)时,它会先将底层数组复制一份,然后在新的数组上进行修改,修改完成后再将指向新数组的引用赋值给原数组,以此来保证集合的线程安全性,在这个过程中,读取操作不需要加锁,可以直接读取原数组中的数据,因此读取性能非常高。
由于写操作需要复制整个底层数组,所以CopyOnWriteArraySet在处理写操作时会有一定的性能开销,但是,在读多写少的场景下,这种开销是可以接受的,因为读取操作的高性能可以弥补写操作的性能损失。
代码案例
下面是一个简单的Java程序,演示了如何使用CopyOnWriteArraySet
类,这段代码展示如何向集合中添加元素、检查元素是否存在以及遍历集合,同时,通过两个线程来模拟并发读写的情况,如下代码所示:
import java.util.concurrent.CopyOnWriteArraySet;
public class CopyOnWriteArraySetExample {
public static void main(String[] args) {
// 创建一个CopyOnWriteArraySet实例
CopyOnWriteArraySet<String> set = new CopyOnWriteArraySet<>();
// 启动一个线程向集合中添加元素
new Thread(() -> {
for (int i = 0; i < 10; i++) {
set.add("Item" + i);
try {
// 模拟一些延迟,使得输出更为明显
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
// 启动另一个线程来读取和打印集合中的元素
new Thread(() -> {
while (set.size() < 10) {
// 打印当前集合中的所有元素
System.out.println("Current set contents: " + set);
try {
// 等待一段时间,以便观察集合的变化
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 最后打印完整的集合内容
System.out.println("Final set contents: " + set);
}).start();
}
}
在代码中,创建了一个CopyOnWriteArraySet
实例,并通过两个线程来操作它,第一个线程向集合中添加10个元素,每次添加后都会稍微延迟一段时间,第二个线程则不断地读取并打印集合的当前内容,直到集合的大小达到10为止。
由于CopyOnWriteArraySet
的写操作(如add
)会创建底层数组的一个新副本,因此即使在添加元素的过程中,读取操作(如第二个线程中的打印操作)也不会被阻塞,并且总是能够看到集合在某个时间点的一致性快照。
核心API
CopyOnWriteArraySet
类实现了 Set
接口,提供了线程安全的集合操作,它是通过在每次修改时复制底层数组来实现线程安全的,因此它特别适合读多写少的场景。以下是 CopyOnWriteArraySet
类中一些主要方法的含义:
1、构造方法
CopyOnWriteArraySet()
:创建一个空的集合。CopyOnWriteArraySet(Collection<? extends E> c)
:创建一个包含指定集合中所有元素的集合。
2、查询操作
int size()
:返回集合中元素的数量。boolean isEmpty()
:如果集合为空,则返回true
。boolean contains(Object o)
:如果集合包含指定的元素,则返回true
。Iterator<E> iterator()
:返回集合中元素的迭代器。Object[] toArray()
:返回一个包含集合中所有元素的数组。<T> T[] toArray(T[] a)
:返回一个包含集合中所有元素的数组,且类型与指定的数组相同。
3、修改操作
boolean add(E e)
:将指定的元素添加到集合中(如果尚未存在)。boolean remove(Object o)
:从集合中移除指定的元素(如果存在)。boolean addAll(Collection<? extends E> c)
:将指定集合中的所有元素添加到当前集合中。boolean removeAll(Collection<?> c)
:从当前集合中移除指定集合中的所有元素(如果存在)。boolean retainAll(Collection<?> c)
:仅保留当前集合中与指定集合中存在的元素。void clear()
:移除集合中的所有元素。
4、集合视图操作(这些方法提供了不同视角的集合视图)
- 由于
CopyOnWriteArraySet
是基于数组的,它并没有直接提供像List
那样的顺序视图,但可以通过迭代器或数组来获取元素。
注意:由于 CopyOnWriteArraySet
的写操作(如 add
, remove
等)是通过复制底层数组来实现的,因此在多线程环境下,这些操作可能相对较慢,但它不会阻塞读操作。
核心总结
CopyOnWriteArraySet
类是一个线程安全的集合,适用于读多写少的并发场景,它在读操作时无需加锁,性能高效,且能保证数据的一致性;因为写操作会复制底层数组,所以也不会阻塞读操作,提高了并发性能。但是它的写操作的开销较大,因为每次修改都需要复制整个数组,在高并发的写操作场景下,性能会受到影响,且会消耗更多内存,此外,CopyOnWriteArraySet
不支持一些 Set
接口的高级功能,如条件查询和批量操作。
技术方案选型上:在读多写少、数据大小适中且对一致性要求较高的场景中,推荐使用CopyOnWriteArraySet
;但在写操作频繁或数据量巨大的情况下,最好还是考虑其它的实现方案。
END!
END!
END!
往期回顾
精品文章
Java并发基础:ConcurrentSkipListMap全面解析
Java并发基础:ConcurrentSkipListSet全面解析!
Java并发基础:SynchronousQueue全面解析!
Java并发基础:ConcurrentLinkedQueue全面解析!