目录
- 一、CopyOnWriteArrayList简介
- 二、CopyOnWriteArrayList的优缺点
- 1、优点
- 2、缺点
- 三、CopyOnWriteArrayList使用场景
- 1、数据库缓存
- 2、消息队列
- 3、数据统计和分析
- 四、使用CopyOnWriteArrayList时需要注意哪些问题?
- 1、内存占用问题
- 2、数据一致性问题
- 3、线程安全
- 4、不支持add()、set()、remove()方法
- 五、下面通过一段代码测试一下CopyOnWriteArrayList的性能。
大家好,我是哪吒。
在上一节中提到了通过ConcurrentHashMap解决HashMap在高并发下数据错乱的问题。
这篇简单介绍一下ArrayList的线程安全类CopyOnWriteArrayList。
一、CopyOnWriteArrayList简介
CopyOnWriteArrayList是ArrayList的一个线程安全的变体。它是通过在对底层数组进行一次新的复制来实现所有可变操作(如add、set等)的。在遍历时,它不会对任何元素进行修改,因此绝对不会抛出ConcurrentModificationException的异常。这种数据结构适合用在“读多,写少”的并发应用中,因为在这种情况下,读操作远远大于写操作,所以使用这种数据结构可以提高并发性能。但是,如果存在大量写操作,使用这种数据结构可能会导致性能下降,因为每次写操作都需要对整个底层数组进行复制。
二、CopyOnWriteArrayList的优缺点
1、优点
- 线程安全:CopyOnWriteArrayList实现了线程安全,可以在多线程环境下正常使用,避免了线程竞争和锁的问题;
- 并发读取性能好:由于CopyOnWriteArrayList在读取数据时不需要进行任何操作,所以在并发读取时效率很高;
- 可用于读多写少的场景:CopyOnWriteArrayList适合用在读多写少的场景中,可以大大提高并发性能。
2、缺点
- 内存占用高:由于CopyOnWriteArrayList每次写操作都需要复制整个底层数组,因此会导致内存占用较高。
- 写操作的延迟高:由于需要进行数据复制,所以CopyOnWriteArrayList的写操作延迟较高。
- 不适用于迭代器的弱一致性需求:CopyOnWriteArrayList的迭代器只能保证最终一致性,不能保证强一致性。因此,如果需要保证迭代器的一致性需求,建议使用其他数据结构。
- 不支持add()、set()、remove()方法:CopyOnWriteArrayList的迭代器实现了ListIterator接口,但是add()、set()、remove()方法都直接抛出了UnsupportedOperationException异常。
三、CopyOnWriteArrayList使用场景
CopyOnWriteArrayList主要适用于读多写少的并发场景,可以在多线程环境下提供高效的读取操作,同时保证线程安全。它适合用于缓存、读操作频繁而写操作较少的场景,例如:
1、数据库缓存
将CopyOnWriteArrayList作为数据库查询结果的缓存,可以避免在多线程环境下频繁地访问数据库,提高查询效率。
2、消息队列
在消息队列中,通常会有大量的读取操作和较少的写入操作,使用CopyOnWriteArrayList可以保证读取操作的并发性和效率。
3、数据统计和分析
在数据统计和分析过程中,通常会有大量的读取操作和较少的写入操作,使用CopyOnWriteArrayList可以提高读取操作的并发性和效率。
需要注意的是,由于CopyOnWriteArrayList的写操作会进行数据复制,可能会导致内存占用较高,因此不适合数据量过大的场景。此外,由于CopyOnWriteArrayList不支持add()、set()、remove()方法,因此也不适合需要频繁进行插入、更新、删除操作的场景。
四、使用CopyOnWriteArrayList时需要注意哪些问题?
1、内存占用问题
由于CopyOnWriteArrayList的写时复制机制,当进行写操作时,内存中会同时驻扎两个对象的内存,旧的对象和新写入的对象。在复制时只复制容器里的引用,在写时才会创建新对象添加到新容器里,而旧容器的对象还在使用,所以有两份对象内存。
2、数据一致性问题
CopyOnWriteArrayList只能保证数据的最终一致性,不能保证数据的实时一致性。因为复制和操作元素需要一些时间,所以会有延迟。如果希望写入的数据马上能读到,要求数据强一致性的话,建议不要使用CopyOnWriteArrayList。
3、线程安全
CopyOnWriteArrayList是写同步,读非同步的。多个线程对CopyOnWriteArrayList进行写操作是线程安全的,但是在读操作时是非线程安全的。如果在for循环中使用下标的方式去读取数据,可能会报错ArrayIndexOutOfBoundsException。
4、不支持add()、set()、remove()方法
CopyOnWriteArrayList的迭代器实现了ListIterator接口,但是add()、set()、remove()方法都直接抛出了UnsupportedOperationException异常,所以应该避免使用迭代器的这几个方法。
五、下面通过一段代码测试一下CopyOnWriteArrayList的性能。
CopyOnWriteArrayList适合读多写少的并发场景,可以提供高效的读取操作和线程安全保障,但需要注意写操作开销较大和内存占用较高的问题。
public static void testWrite() {
List<String> copyOnWriteArrayList = new CopyOnWriteArrayList<>();
List<String> synchronizedList = Collections.synchronizedList(new ArrayList<>());
StopWatch stopWatch = new StopWatch();
stopWatch.start();
IntStream.rangeClosed(1, 100000).parallel().forEach(x -> copyOnWriteArrayList.add(UUID.randomUUID().toString()));
stopWatch.stop();
System.out.println("CopyOnWriteArrayList 写方法耗时:"+stopWatch.getTotalTimeSeconds());
StopWatch stopWatch2 = new StopWatch();
stopWatch2.start();
IntStream.rangeClosed(1, 100000).parallel().forEach(x -> synchronizedList.add(UUID.randomUUID().toString()));
stopWatch2.stop();
System.out.println("List 写方法耗时:"+stopWatch2.getTotalTimeSeconds());
}
public static void testRead() {
List<Integer> copyOnWriteArrayList = new CopyOnWriteArrayList<>();
List<Integer> synchronizedList = Collections.synchronizedList(new ArrayList<>());
copyOnWriteArrayList.addAll(IntStream.rangeClosed(1, 1000000).boxed().collect(Collectors.toList()));
synchronizedList.addAll(IntStream.rangeClosed(1, 1000000).boxed().collect(Collectors.toList()));
StopWatch stopWatch = new StopWatch();
stopWatch.start();
IntStream.range(1, 1000000).parallel().forEach(x -> copyOnWriteArrayList.get(x));
stopWatch.stop();
System.out.println("copyOnWriteArrayList 读方法耗时:"+stopWatch.getTotalTimeSeconds());
StopWatch stopWatch2 = new StopWatch();
stopWatch2.start();
IntStream.range(1, 1000000).parallel().forEach(x -> synchronizedList.get(x));
stopWatch2.stop();
System.out.println("List 读方法耗时:"+stopWatch2.getTotalTimeSeconds());
}
在高并发场景下,CopyOnWriteArrayList 写的速度比ArrayList加锁的方式足足慢了100倍,读的效率相差无几。印证了上面的结论,CopyOnWriteArrayList 适用于**“读多,写少”**的并发应用中。
上一篇:一个关于 i++ 和 ++i 的面试题打趴了所有人
🏆哪吒多年工作总结:Java学习路线总结,搬砖工逆袭Java架构师。