文章目录
- 一、CopyOnWriteArrayList的简介
- 二、CopyOnWriteArrayList类的继承关系
- 1、Iterable接口:
- 2、Collection接口:
- 3、List接口:
- 4、Cloneable接口:
- 5、Serializable接口:
- 6、RandomAccess接口:
- 三、CopyOnWriteArrayList的基本使用
- 1. 使用场景:
- 1.1、读多写少的情况:
- 1.2、数据一致性要求较低:
- 1.3、需要遍历操作:
- 2. 代码实现:
- 3. 运行结果:
- 4. 案例分析:
- 四、CopyOnWriteArrayList的优缺点
- 1. 优点:
- 1.1、线程安全:
- 1.2、并发读取性能好:
- 1.3、可用于读多写少的场景:
- 2. 缺点:
- 2.1、内存占用高:
- 2.2、写操作的延迟高:
- 2.3、不适用于迭代器的弱一致性需求:
- 3. 总结:
- 五、底层原理分析
- 1. add方法
- 2. get方法
一、CopyOnWriteArrayList的简介
-
CopyOnWriteArrayList是Java中的一种并发集合类,它实现了List接口,并且是线程安全的。它的特点是在进行写操作时,会创建一个新的副本来进行操作,而不是直接修改原有的集合。
-
CopyOnWriteArrayList的实现机制是在写操作时,先将原有的集合进行复制,然后对新的副本进行修改操作,最后将修改后的副本替换原有的集合。这样可以保证在写操作时不会影响到正在进行读操作的线程,保证了读写的并发性。
-
由于每次写操作都会复制整个集合,所以CopyOnWriteArrayList的写操作性能较低,适用于读操作较多而写操作较少的场景。它适合用于一些读多写少的并发场景,比如缓存、观察者模式等。
-
需要注意的是,CopyOnWriteArrayList虽然是线程安全的,但是它的迭代器并不支持修改操作,如果需要对集合进行修改,需要使用特殊的方法来操作。此外,由于每次写操作都会创建一个新的副本,所以CopyOnWriteArrayList占用的内存较大,需要根据具体场景进行权衡。
二、CopyOnWriteArrayList类的继承关系
1、Iterable接口:
CopyOnWriteArrayList类实现了Iterable接口,使得它可以被迭代遍历。通过实现iterator()方法,可以获取一个迭代器,用于遍历集合中的元素。
2、Collection接口:
CopyOnWriteArrayList类继承了AbstractCollection类,间接实现了Collection接口。通过实现Collection接口,CopyOnWriteArrayList类获得了一些常用的集合操作方法,比如判断是否包含某个元素、计算集合的大小等。
3、List接口:
CopyOnWriteArrayList类间接实现了List接口,通过继承AbstractList类实现了List接口的一些方法。List接口定义了有序的集合,CopyOnWriteArrayList类通过继承List接口,可以按照索引进行访问和操作集合中的元素。
4、Cloneable接口:
CopyOnWriteArrayList类实现了Cloneable接口,使得它可以进行克隆操作。通过实现clone()方法,可以创建CopyOnWriteArrayList对象的副本。
5、Serializable接口:
CopyOnWriteArrayList类实现了Serializable接口,使得它可以进行序列化操作。通过实现writeObject()和readObject()方法,可以将CopyOnWriteArrayList对象转换为字节流,并进行存储或传输。
6、RandomAccess接口:
CopyOnWriteArrayList类实现了RandomAccess接口,表示它可以高效地随机访问元素。RandomAccess接口是一个标记接口,用于标识实现该接口的类可以通过索引进行快速随机访问,CopyOnWriteArrayList类通过实现该接口,表明它可以高效地随机访问元素。
三、CopyOnWriteArrayList的基本使用
1. 使用场景:
CopyOnWriteArrayList的主要使用场景是在多线程环境下,需要读多写少的情况下使用。
由于CopyOnWriteArrayList是线程安全的,它通过在写操作时创建一个新的数组来实现并发安全。在写操作时,原有数组不发生改变,而是将写操作应用于新的数组。这样就避免了读操作和写操作的冲突,保证了线程安全。
CopyOnWriteArrayList适用于以下场景:
1.1、读多写少的情况:
由于写操作会创建新的数组,因此写操作的开销较大。如果写操作很频繁,会导致性能下降。因此,在读多写少的情况下,CopyOnWriteArrayList能够提供较好的性能。
1.2、数据一致性要求较低:
由于写操作不会影响原来的数组,读操作总是读取最新的数组。因此,如果对数据的实时性要求不高,可以使用CopyOnWriteArrayList。
1.3、需要遍历操作:
CopyOnWriteArrayList适合于需要频繁进行遍历操作的场景。由于读操作不需要加锁,所以可以并发地进行遍历操作,提高了遍历的效率。
需要注意的是,CopyOnWriteArrayList适用于静态数据,即数据一旦初始化就不会修改的情况。如果数据需要频繁修改,则不适合使用CopyOnWriteArrayList。此外,由于每次写操作都会创建一个新的数组,占用内存较大,所以不适合数据量过大的情况。
2. 代码实现:
import java.util.Iterator;
import java.util.concurrent.CopyOnWriteArrayList;public class CopyOnWriteArrayListDemo {public static void main(String[] args) {// 创建一个CopyOnWriteArrayList对象CopyOnWriteArrayList<Integer> list = new CopyOnWriteArrayList<>();// 添加元素list.add(1);list.add(2);list.add(3);// 创建一个线程用于遍历元素Thread iteratorThread = new Thread(() -> {Iterator<Integer> iterator = list.iterator();while (iterator.hasNext()) {System.out.println(iterator.next());try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}});// 创建一个线程用于修改元素Thread modifyThread = new Thread(() -> {try {Thread.sleep(500);} catch (InterruptedException e) {e.printStackTrace();}list.add(4);list.remove(1);});// 启动两个线程iteratorThread.start();modifyThread.start();}
}
3. 运行结果:
4. 案例分析:
-
在上面的代码中,我们首先创建了一个CopyOnWriteArrayList对象,并向其中添加了一些元素。然后,我们创建了两个线程:一个线程用于遍历元素,另一个线程用于修改元素。在遍历线程中,我们使用iterator()方法获取迭代器,并通过调用next()方法来遍历元素。在修改线程中,我们在添加和删除元素之间添加了一个延迟,以便观察到遍历线程的输出结果。
-
由于CopyOnWriteArrayList是线程安全的,即使在遍历的同时进行元素的修改,也不会抛出ConcurrentModificationException异常。这是因为在修改操作时,CopyOnWriteArrayList会创建一个新的数组来存储修改后的元素,而原来的数组不会受到影响。因此,遍历线程可以继续使用原来的数组进行遍历,保证了遍历的安全性。
四、CopyOnWriteArrayList的优缺点
1. 优点:
1.1、线程安全:
CopyOnWriteArrayList是线程安全的,多个线程可以同时读取列表中的数据而无需额外的同步机制。
1.2、并发读取性能好:
由于读操作不需要加锁,多个线程可以同时读取,提高了并发读取的性能。
1.3、可用于读多写少的场景:
适用于读操作频繁,写操作较少的场景。
2. 缺点:
2.1、内存占用高:
每次写操作都会创建一个新的数组,导致内存占用较高。如果写操作频繁,会消耗大量的内存。
2.2、写操作的延迟高:
由于每次写操作都会复制整个数组,写操作的执行时间较长,可能会导致写操作的延迟较高。
2.3、不适用于迭代器的弱一致性需求:
CopyOnWriteArrayList的迭代器是弱一致性的,即迭代器在创建之后不会反映后续的修改操作。这可能会导致迭代器在遍历过程中出现数据不一致的情况。
3. 总结:
综上所述,CopyOnWriteArrayList适用于读多写少的场景,对于需要高并发性能和线程安全的读操作非常有用。然而,它的内存占用高和写操作的延迟高可能会影响性能,在弱一致性需求较高的情况下也不适合使用。
五、底层原理分析
1. add方法
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();}}
- 获取一把独占多lock ,加锁
- 获取原数组长度,创建一个原数组长度+1的新数组
- 利用Arrays.copyOf将元素进行复制到新的数组
- 将添加的元素设置到新开辟数组的最后一个位置
- 修改老的数组的引用指向
- 释放锁
2. get方法
public E get(int index) {return get(getArray(), index);}private E get(Object[] a, int index) {return (E) a[index];}
- 根据下标获取指定位置元素,可以看到读不加锁