深入浅出 JAVA 之集合 - CopyOnWriteArrayList
文章目录
!版权声明:本博客内容均为原创,每篇博文作为知识积累,写博不易,转载请注明出处。
系统环境
- JDK 版本: OpenJDK 8
参考地址:
一、CopyOnWriteArrayList 简介
CopyOnWriteArrayList 是一种线程安全的 List 接口实现,它采用了读写分离的思想,即读取操作和修改操作分别在不同的内部数组中进行,从而实现了读取操作的高效性和修改操作的线程安全性。
具体来说,当 CopyOnWriteArrayList 被修改时,它会创建一个新的内部数组,并将修改操作应用于这个新的数组,而不是直接在原数组上进行修改。这个过程虽然会增加内存的开销,但由于读取操作并不会被加锁,所以可以允许多个线程同时进行读取操作,从而提高了整个程序的并发性能。
注: 由于 CopyOnWriteArrayList 将修改操作应用于新的数组上,因此在进行迭代操作时,迭代器可能会看不到最新的修改,因为迭代器仍然指向旧的数组。因此,CopyOnWriteArrayList 适用于读取操作远远多于修改操作的场合,而不适合频繁进行修改操作。
二、CopyOnWriteArrayList 主要特性
CopyOnWriteArrayList 的主要特性如下:
- 线程安全: CopyOnWriteArrayList是线程安全的,多个线程可以同时读取内部数组,而不会出现数据覆盖等并发问题。
- 读写分离: CopyOnWriteArrayList采用了读写分离的思想,即读取操作和修改操作分别在不同的内部数组中进行,从而提高了读取操作的效率。
- 内存开销较大: 由于每次修改都会创建一个新的内部数组,因此CopyOnWriteArrayList的内存开销比较大,特别是在修改操作比较频繁的情况下。
- 迭代器不支持修改操作: 由于迭代器依赖于内部数组,并且每次修改都会创建一个新的内部数组,因此迭代器不支持修改操作,否则会抛出ConcurrentModificationException异常。
- 适用于读取操作远远多于修改操作的场合: 由于每次修改都会创建一个新的内部数组,因此CopyOnWriteArrayList适用于读取操作远远多于修改操作的场合,特别是基于事件的编程模型中的回调函数列表等场合
ArrayList 的优缺点是什么
三、CopyOnWriteArrayList 的常见操作方法有哪些
ConcurrentArrayList 扩展了标准的 ArrayList ,提供了线程安全的读写操作,以下是一些常见的操作方法:
- add(E e): 添加元素到集合末尾。
- add(int index, E element): 在指定位置插入元素。
- addAll(Collection<? extends E> c): 将一个集合中的所有元素添加到列表末尾。
- get(int index): 获取指定位置上的元素。
- indexOf(Object o): 返回指定元素在列表中第一次出现的位置。
- isEmpty(): 判断列表是否为空。
- remove(Object o): 从列表中删除指定元素。
- remove(int index): 从列表中删除指定位置的元素。
- set(int index, E element): 将指定位置的元素替换为指定元素。
- size(): 返回列表的大小。
需要注意的是, ConcurrentArrayList 的常见操作方法和 ArrayList 大致相同,但是在多线程环境下它们提供了不同的线程安全保障。
四、CopyOnWriteArrayList 的优缺点是什么
CopyOnWriteArrayList 的优点和缺点如下:
CopyOnWriteArrayList 的优点:
- 线程安全: CopyOnWriteArrayList 是线程安全的,多个线程可以并发读取数据,而不需要额外的同步机制。
- 高效: CopyOnWriteArrayList 在读多写少的场景下表现优异,因为写操作并不会影响到读操作,读操作也不会被锁定,从而提高了系统的并发性能。
- 不需要加锁: CopyOnWriteArrayList 在写操作时创建新的数组,读操作仍旧在老的数组中进行,因此它不需要额外的加锁机制,避免了线程间的竞争,提高了系统的并发性能。
CopyOnWriteArrayList 的缺点:
- 内存占用高: 由于每次写操作都会新建一个数组,因此内存占用会比较高。如果写操作很频繁,那么会造成资源浪费。
- 读操作不能实时反映最新结果: 由于写操作不会对老数组进行修改,因此在写操作完成之前,读操作只能看到老数组中的数据,不能及时反映最新的结果。
- 不适合实时性要求高的场景: 由于读操作不能实时反映最新结果,因此它并不适合实时性要求比较高的场景,例如金融交易等场景。
五、CopyOnWriteArrayList 如何保证线程安全
CopyOnWriteArrayList 通过使用 "写时复制" 的方式来保证线程安全。
在每次写操作时,它都会创建一个新的数组,并将原有数组中的元素复制到新数组中,然后再在新数组中进行写操作。
通过这种方式,读操作可以在原有数组中进行,并且不需要额外的同步机制来保证线程之间的安全性。只有在写操作完成后,才会将新数组赋值给原有数组,从而完成一次写操作。
这种写时复制的方式,保证了写操作和读操作的并发执行,从而提高了系统的并发性能。但是需要注意的是,这种方式会导致内存占用较高,因为每个副本都需要占用一定的内存空间。
六、CopyOnWriteArrayList 底层结构
CopyOnWriteArrayList 底层结构是一个数组,它是通过 volatile 修饰的 Object[] 数组来存储元素的,即:
1private transient volatile Object[] array;
在 CopyOnWriteArrayList 中,读取操作和修改操作是在不同的数组上进行的,即读取操作是在当前的 array 数组上进行的,而修改操作是在新的数组上进行的。当进行修改操作时,CopyOnWriteArrayList 会创建一个新的数组,将原来的元素复制到新的数组中,然后再进行修改操作,最后将新的数组的引用赋值给 array,完成修改操作。当进行读取操作时,CopyOnWriteArrayList 则直接在当前的 array 数组上进行。
七、CopyOnWriteArrayList 源码 - 属性
1/** 可重入锁对象 */
2final transient ReentrantLock lock = new ReentrantLock();
3
4/** 存储元素的数组 */
5private transient volatile Object[] array;
八、CopyOnWriteArrayList 源码 - 关键方法
8.1 构造方法 CopyOnWriteArrayList
(1) 无参构造方法
1/**
2 * 无参构造方法,创建一个空数组
3 */
4public CopyOnWriteArrayList() {
5 setArray(new Object[0]);
6}
(2) 支持根据已有 Collection 集合创建新集合的构造方法
1/**
2 * 支持根据已有 Collection 集合创建新集合的构造方法
3 *
4 * @param c 已有的 Collection 集合
5 * @throws NullPointerException 空指针异常
6 */
7public CopyOnWriteArrayList(Collection<? extends E> c) {
8 // 创建新数组
9 Object[] elements;
10 // 判断传入的集合是否为 CopyOnWriteArrayList 类型:
11 // - 如果是,则直接将传入集合的数组作为新集合的数组;
12 // - 如果不是,则将集合转换为数组来充当新集合的数组,然后判断传入集合是否为 ArrayList 类型,
13 // 如果不是则将元素转换为 Object[] 类型;
14 if (c.getClass() == CopyOnWriteArrayList.class) {
15 elements = ((CopyOnWriteArrayList<?>)c).getArray();
16 } else {
17 elements = c.toArray();
18 if (c.getClass() != java.util.ArrayList.class) {
19 // 因为 ArrayList 集合中的数组为 Object[] 类型,但是如果集合不是 ArrayList 类型,
20 // 则数组元素类型可能不是 Object 类型,所以这里需要执行一遍复制转换过程;
21 elements = Arrays.copyOf(elements, elements.length, Object[].class);
22 }
23 }
24 // 设置新创建的数组作为集合存储元素的数组
25 setArray(elements);
26}
(3) 支持根据已有数组创建新集合的构造方法
1/**
2 * 支持根据已有数组创建新集合的构造方法
3 *
4 * @param toCopyIn 已有的数组
5 * @throws NullPointerException 空指针异常
6 */
7public CopyOnWriteArrayList(E[] toCopyIn) {
8 // 创建新数组,并从传入数组中复制元素且转换位 Object 类型到新数组中
9 setArray(Arrays.copyOf(toCopyIn, toCopyIn.length, Object[].class));
10}
8.2 末尾位置插入元素方法 add(E e)
add(E e)
1/**
2 * 在集合末尾插入一个元素
3 *
4 * @param e 插入的元素
5 * @return 插入成功则返回 true
6 */
7public boolean add(E e) {
8 // 获取可重入锁对象
9 final ReentrantLock lock = this.lock;
10 // 上锁
11 lock.lock();
12 try {
13 // 获取集合存储元素的数组
14 Object[] elements = getArray();
15 // 获取元素数组长度赋给贝变量 len
16 int len = elements.length;
17 // 创建一个长度为 len+1 的新数组,并且将原先数据转移到新数组中
18 Object[] newElements = Arrays.copyOf(elements, len + 1);
19 // 将新插入的元素添加到数组末尾索引位置
20 newElements[len] = e;
21 // 设置新创建的数组作为集合存储元素的数组
22 setArray(newElements);
23 // 插入成功,返回 true
24 return true;
25 } finally {
26 // 解锁
27 lock.unlock();
28 }
29}
getArray()
1/**
2 * 获取集合存储元素的数组
3 */
4final Object[] getArray() {
5 return array;
6}
8.3 指定位置插入元素方法 add(int index, E element)
1/**
2 * 指定位置插入元素方法
3 *
4 * @param index 插入的索引位置
5 * @param element 要插入的元素
6 * @throws IndexOutOfBoundsException 索引越界异常
7 */
8public void add(int index, E element) {
9 // 获取可重入锁对象
10 final ReentrantLock lock = this.lock;
11 // 上锁
12 lock.lock();
13 try {
14 // 获取集合存储元素的数组
15 Object[] elements = getArray();
16 // 获取元素数组长度赋给贝变量 len
17 int len = elements.length;
18 // 判断待插入的索引是否在元素数组长度范围内,如果不在则抛出数组越界异常
19 if (index > len || index < 0) {
20 throw new IndexOutOfBoundsException("Index: "+index+ ", Size: "+len);
21 }
22 // 创建记录新数组的变量
23 Object[] newElements;
24 // 计算需要从指定索引位置开始往后挪移的元素数量
25 int numMoved = len - index;
26 // 判断挪移数量是否为 0:
27 // - 如果数量为 0,则相当于在数组末尾插入新元素,则直接创建一个长度为 len+1 的新数组,并且将原先数据转移到新数组中,
28 // 最后将新插入的元素添加到数组指定索引位置;
29 // - 如果数量不为 0,则相当在数组指定位置插入新元素,则创建一个 len+1 的新数组,然后复制索引位置之前的数据到新数组中,
30 // 再复制索引位置之后的数据到新数组中,最后将新插入的元素添加到数组指定索引位置;
31 if (numMoved == 0) {
32 newElements = Arrays.copyOf(elements, len + 1);
33 } else {
34 newElements = new Object[len + 1];
35 System.arraycopy(elements, 0, newElements, 0, index);
36 System.arraycopy(elements, index, newElements, index + 1, numMoved);
37 }
38 newElements[index] = element;
39 // 设置新创建的数组作为集合存储元素的数组
40 setArray(newElements);
41 } finally {
42 // 解锁
43 lock.unlock();
44 }
45}
8.4 删除指定位置元素方法 remove(int index)
1/**
2 * 删除指定位置元素
3 *
4 * @throws IndexOutOfBoundsException {@inheritDoc}
5 */
6public E remove(int index) {
7 // 获取可重入锁对象
8 final ReentrantLock lock = this.lock;
9 // 上锁
10 lock.lock();
11 try {
12 // 获取集合存储元素的数组
13 Object[] elements = getArray();
14 // 获取元素数组长度赋给贝变量 len
15 int len = elements.length;
16 // 获取待删除元素位置的元素值赋给变量 oldValue
17 E oldValue = get(elements, index);
18 // 计算需要挪移的元素数量
19 int numMoved = len - index - 1;
20 // 判断挪移数量是否为 0:
21 // - 如果数量为 0,则相当于在数组末尾删除元素,则直接创建一个长度为 len-1 的新数组,这样相当于删除了末尾的元素;
22 // - 如果数量不为 0,则相当在数组指定位置删除元素,则创建一个 len-1 的新数组,然后复制索引位置之前的数据到新数组中,
23 // 再复制索引位置之后的数据到新数组中,唯独不复制待删除索引位置的元素,这样相当于删除了该元素;
24 if (numMoved == 0) {
25 setArray(Arrays.copyOf(elements, len - 1));
26 } else {
27 Object[] newElements = new Object[len - 1];
28 System.arraycopy(elements, 0, newElements, 0, index);
29 System.arraycopy(elements, index + 1, newElements, index, numMoved);
30 // 设置新创建的数组作为集合存储元素的数组
31 setArray(newElements);
32 }
33 // 返回删除前待删除索引位置的元素值
34 return oldValue;
35 } finally {
36 lock.unlock();
37 }
38}
8.5 删除指定元素值的元素方法 remove(Object o)
remove(Object o)
1/**
2 * 删除指定元素值的元素
3 *
4 * @param o 待删除的元素 (如果存在)
5 * @return 如果删除成功则返回 true,否则返回 false
6 */
7public boolean remove(Object o) {
8 // 创建变量 snapshot 存储集合当前存储元素的数组
9 Object[] snapshot = getArray();
10 // 查找指定元素值在数组中的索引
11 int index = indexOf(o, snapshot, 0, snapshot.length);
12 // 判断索引是否小于 0:
13 // - 如果是,则说明在数组中没有找到该索引值,index=-1,所以返回 false;
14 // - 如果否则说明在数组中找到了该索引值,这时候调用 remove 方法从数组中删除该元素;
15 return (index < 0) ? false : remove(o, snapshot, index);
16}
indexOf(Object o, Object[] elements, int index, int fence)
1/**
2 * 查找指定元素索引
3 *
4 * @param o 待查找的元素值
5 * @param elements 待查找的元素数组
6 * @param index 查找的起始索引位置
7 * @param fence 查找的结束索引位置
8 * @return 返回查找到的索引,如果没有找到则返回 -1
9 */
10private static int indexOf(Object o, Object[] elements, int index, int fence) {
11 // 判断待查找的元素值是否为 null:
12 // - 如果为 null,则从 i 索引位置开始遍历,到 fence 位置截止,查找元素值为 null 的索引;
13 // - 如果不为 null,则从 i 索引位置开始遍历,到 fence 位置截止,查找元素值为 o 的索引;
14 if (o == null) {
15 for (int i = index; i < fence; i++) {
16 if (elements[i] == null) {
17 return i;
18 }
19 }
20 } else {
21 for (int i = index; i < fence; i++) {
22 if (o.equals(elements[i])) {
23 return i;
24 }
25 }
26 }
27 // 没有找到返回 -1
28 return -1;
29}
remove(Object o, Object[] snapshot, int index)
1/**
2 * 删除指定数组中指定索引位置和元素值的元素
3 */
4private boolean remove(Object o, Object[] snapshot, int index) {
5 // 获取可重入锁对象
6 final ReentrantLock lock = this.lock;
7 // 上锁
8 lock.lock();
9 try {
10 // 获取当前集合存储元素的数组
11 Object[] current = getArray();
12 // 获取元素数组长度赋给贝变量 len
13 int len = current.length;
14 // 判断 snapshot 变量记录的数组是否和 current 变量记录的当前数组相同,
15 // - 如果不相同,则在新数组中查找元素值的索引位置;
16 // - 如果相同,则执行后续的数组数据转移和删除操作;
17 if (snapshot != current) {
18 // 设置标签,当 : 中 {} 里面的代码逻辑满足后执行 continue 或者 break 时,
19 // 后面加标签,这样在执行完 continue 或者 break 操作后就会回到标签的位置,
20 // 不再执行 continue 或 break 后面的代码逻辑,直接执行标签的下一行代码。
21 // 用法类似于 C/C++ 中的 goto 关键字
22 findIndex:
23 {
24 // 使待查找的索引和当前数组长度进行比较,获取其中较小的值,作为下一步循环的结束位置
25 int prefix = Math.min(index, len);
26 // 进行循环,使待删除的元素值与当前集合元素数组中的元素进行比较,以 prefix 为遍历结束点,
27 // 然后判断是否相同,如果相同则直接设置循环到的位置作为索引,然后执行 break findIndex,
28 // 回到 findIndex 代码位置
29 for (int i = 0; i < prefix; i++) {
30 if (current[i] != snapshot[i] && eq(o, current[i])) {
31 index = i;
32 break findIndex;
33 }
34 }
35 // 判断待删除的索引是否大于当前数组长度,如果是则表示遍历新数组也没有找到待删除元素的索引位置,
36 // 则返回 false,表示删除失败,该元素可能已经被删除。
37 if (index >= len) {
38 return false;
39 }
40 // 如果数组 index 位置正好与待删除的索引值内存地址相同,则直接执行 break findIndex,回到
41 // findIndex 代码位置
42 if (current[index] == o) {
43 break findIndex;
44 }
45 // 再次调用 indexOf 方法从当前数组中查找待删除元素值的索引
46 index = indexOf(o, current, index, len);
47 // 如果 index=-1,即小于 0,则说明当前数组中没有找到这个待删除的元素值,可能已经删除掉了,
48 // 这里直接返回 false
49 if (index < 0) {
50 return false;
51 }
52 }
53 }
54 // 创建一个长度为 len-1 的新数组,然后复制索引位置之前的数据到新数组中,再复制索引位置之后的数据到新数组中,
55 // 唯独不复制待删除索引位置的元素,这样相当于删除了该元素;
56 Object[] newElements = new Object[len - 1];
57 System.arraycopy(current, 0, newElements, 0, index);
58 System.arraycopy(current, index + 1, newElements, index, len - index - 1);
59 // 设置新创建的数组作为集合存储元素的数组
60 setArray(newElements);
61 // 返回 true
62 return true;
63 } finally {
64 // 解锁
65 lock.unlock();
66 }
67}
8.6 获取指定索引位置元素值方法 get(int index)
get(int index)
1/**
2 * 获取指定索引位置的元素值
3 *
4 * @param index 索引位置
5 * @return 指定索引位置的元素值
6 */
7public E get(int index) {
8 // 获取当前集合存储元素的数组,然后调用 get(Object[] a, int index) 方法,获取指定索引位置元素
9 return get(getArray(), index);
10}
get(Object[] a, int index)
1/**
2 * 获取指定数组下指定位置的元素值
3 *
4 * @param a 待操作的数组
5 * @param index 指定的索引位置
6 * @return 指定索引位置的元素值
7 */
8@SuppressWarnings("unchecked")
9private E get(Object[] a, int index) {
10 return (E) a[index];
11}
8.7 设置指定索引位置元素值方法 set(int index, E element)
set(int index, E element)
1/**
2 * 设置指定索引位置元素值
3 *
4 * @param index 索引
5 * @param element 元素值
6 * @return 先前位于指定位置的元素
7 * @throws IndexOutOfBoundsException 索引越界异常
8 */
9public E set(int index, E element) {
10 // 获取可重入锁对象
11 final ReentrantLock lock = this.lock;
12 // 上锁
13 lock.lock();
14 try {
15 // 获取集合存储元素的数组
16 Object[] elements = getArray();
17 // 获取待替换元素位置的元素值赋给变量 oldValue
18 E oldValue = get(elements, index);
19 // 判断待替换索引位置的元素是否和新元素相同:
20 // - 如果不相同,则创建一个新数组,然后转移旧数组中的元素,之后将新数组指定索引位置的元素替换为新元素,
21 // 最好设置新创建的数组作为集合存储元素的数组
22 // - 如果相同,则继续将旧数组作为集合存储元素的数组
23 if (oldValue != element) {
24 int len = elements.length;
25 Object[] newElements = Arrays.copyOf(elements, len);
26 newElements[index] = element;
27 setArray(newElements);
28 } else {
29 setArray(elements);
30 }
31 // 返回先前位于指定位置的元素
32 return oldValue;
33 } finally {
34 // 解锁
35 lock.unlock();
36 }
37}
九、ConcurrentArrayList 中的疑问
9.1 ConcurrentArrayList 是否有性能问题?
9.2 ConcurrentArrayList 如何保证线程安全?
!版权声明:本博客内容均为原创,每篇博文作为知识积累,写博不易,转载请注明出处。