点击 Mr.绵羊的知识星球 解锁更多优质文章。
目录
六、CopyOnWriteArrayList和ArrayList区别
一、介绍
CopyOnWriteArrayList是Java集合框架中的一种并发容器。它是ArrayList的线程安全版本,采用了一种特殊的“写时复制”策略,即每次修改操作都会先复制一份当前集合,然后再对新的副本进行修改,最后再用新的副本替换旧的副本。这种策略能够保证并发操作时不会影响到其他线程的读操作,从而保证线程安全性。
二、特性
1. 线程安全
CopyOnWriteArrayList是线程安全的,并发访问时不需要使用额外的同步机制,因为每个线程访问的都是不同的副本。
2. 高效读取
由于读取操作不需要加锁,所以CopyOnWriteArrayList在读取操作上具有很高的性能。
3. 低效写入
每次修改都需要复制一份新的副本,因此写入操作相对较慢,特别是在集合较大的情况下。
4. 适合读多写少的场景
CopyOnWriteArrayList适合于读多写少的场景,例如日志、观察者模式等。因为写入操作会产生较大的开销,所以在高并发写入的场景下不适合使用。
三、实现原理
CopyOnWriteArrayList采用了“写时复制”的策略,每次修改都会先复制一份当前集合,然后再对新的副本进行修改。这个过程中,其他线程的读操作仍然是访问旧的副本,直到修改完成后再使用新的副本替换旧的副本。
四、注意事项
1. 内存占用
由于每次修改都需要复制一份新的副本,因此CopyOnWriteArrayList在内存占用上相对较高,尤其是在集合较大的情况下。
2. 迭代器不支持修改操作
CopyOnWriteArrayList的迭代器不支持修改操作,如果需要修改集合中的元素,需要使用集合本身提供的修改方法。
3. 不适用于实时数据
由于CopyOnWriteArrayList的写入操作有一定的延迟,因此不适用于实时数据的场景,例如股票、期货等实时数据的更新。
4. 并发性能问题
虽然CopyOnWriteArrayList在读操作上具有良好的并发性能,但是在写操作上由于每次修改都需要复制一份新的数组,因此性能会受到影响。因此在高并发场景下,建议使用其他的并发容器,例如ConcurrentHashMap等。
5. 迭代器弱一致性问题
由于CopyOnWriteArrayList的迭代器是在快照数组上进行遍历,因此可能存在迭代器遍历到的数据已经被其他线程修改的情况。因此需要注意CopyOnWriteArrayList的迭代器只提供弱一致性的保证,不支持并发修改操作。
五、应用场景
CopyOnWriteArrayList适合于读多写少的场景,例如日志、观察者模式等。同时,由于它的写入操作相对较慢,在高并发写入的场景下不适合使用。
六、CopyOnWriteArrayList和ArrayList区别
1. 线程安全性
ArrayList是非线程安全的容器,如果多个线程同时对其进行读写操作,可能会导致数据不一致的问题。
CopyOnWriteArrayList是线程安全的容器,可以保证在并发环境下数据的一致性。
2. 内存占用
ArrayList的元素存储在一个连续的数组中,如果需要在数组中间插入或删除元素,则需要对数组进行扩容或移动,会造成一定的开销。
CopyOnWriteArrayList在每次写入时都会复制一份新的副本,因此会占用较多的内存空间。
3. 写入操作的开销
ArrayList在进行插入、删除操作时,需要对数组进行移动或扩容,操作的开销较大。
CopyOnWriteArrayList在进行写入操作时,需要先进行一次数组复制,然后再对新数组进行修改,开销也比较大。
因此,如果需要在高并发环境下使用集合,可以考虑使用CopyOnWriteArrayList;如果不需要线程安全性,并且写操作比较频繁,可以选择使用ArrayList。
具体实现可以看下面的CopyOnWriteArrayList伪代码:
class CopyOnWriteArrayList<T> {
// 一个可重入锁,用于实现线程安全
final transient ReentrantLock lock = new ReentrantLock();
/** The array, accessed only via getArray/setArray. */
// 只能通过 getArray/setArray 方法访问的基础数组
private transient volatile Object[] array;
public boolean add(E e) {
// 获取可重入锁实例,以保证在一个时刻只有一个线程可以修改列表
final ReentrantLock lock = this.lock;
lock.lock(); // 获取锁
try {
// 获取当前基础数组
Object[] elements = getArray();
// 获取当前基础数组的长度
int len = elements.length;
// 创建一个新数组,长度比当前数组大1
Object[] newElements = Arrays.copyOf(elements, len + 1);
// 将新元素添加到新数组的最后一个位置
newElements[len] = e;
// 将基础数组更新为新数组
setArray(newElements);
return true;
} finally {
// 释放锁
lock.unlock();
}
}
}
在add方法中,每次修改都会复制一份新的副本,并对新的副本进行修改,最后再用新的副本替换旧的副本。这个过程中使用了ReentrantLock关键字来保证线程安全。
七、实际应用
1.案例一
(1) 场景
验证CopyOnWriteArrayList和ArrayList在多线程情况下的区别。
(2) 代码
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
/**
* CopyOnWriteArrayListCase1: CopyOnWriteArrayList相对于ArrayList有什么根本区别呢?
* CopyOnWriteArrayList: 线程安全。
* ArrayList: 线程不安全。
* 下面这个案例就是验证CopyOnWriteArrayList和ArrayList在多线程情况下修改的区别。
*
* @author wxy
* @since 2023-04-26
*/
public class CopyOnWriteArrayListCase1 {
public static void main(String[] args) {
// 创建一个CopyOnWriteArrayList
// List<Integer> numbers = new CopyOnWriteArrayList<>();
// 创建一个ArrayList
List<Integer> numbers = new ArrayList<>();
// 添加元素到该ArrayList中
numbers.add(1);
numbers.add(2);
numbers.add(3);
numbers.add(4);
numbers.add(5);
// 启动第一个线程,修改List元素,每2秒执行一次。
new Thread(() -> {
try {
while (true) {
//执行修改操作,增加一个元素。
numbers.add(6);
//暂停2秒
TimeUnit.SECONDS.sleep(1);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
//启动第二个线程,遍历List,每1秒执行一次。
new Thread(() -> {
try {
while (true) {
for (Integer number : numbers) {
// System.out.println("used CopyOnWriteArrayList: num: " + number);
System.out.println("used ArrayList: num: " + number);
}
//暂停1秒
TimeUnit.SECONDS.sleep(1);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
}
}
在以上程序中,我们首先创建了一个名为numbers的ArrayList,并在其中添加了一些元素。接着,我们启动了两个线程,一个线程每1秒向ArrayList中添加元素,另一个线程每1秒遍历ArrayList中的元素并打印它们。由于这两个线程都同时访问同一个ArrayList,因此会发生并发修改异常,导致程序抛出ConcurrentModificationException异常,从而使程序崩溃。
如果我们使用CopyOnWriteArrayList,就不会出现这个问题!