CopyOnWriteArrayList
是 Java 并发包 java.util.concurrent
中提供的一个线程安全的列表实现。它的设计理念是:当列表需要被修改时(添加、设置、删除操作),不是直接在原有的数组上进行修改,而是先复制原数组,在复制的数组上进行修改,然后将原数组指向新数组。由于写操作是在复制的新数组上进行的,因此不会影响读操作的进行,从而实现读写分离,保证了读操作的高性能。
具有以下特点:
- 线程安全:所有修改操作(如
add
,set
等)都是通过创建底层数组的一个新副本来实现的。这意味着在修改时不会影响正在进行的迭代操作,从而保证了线程安全性。 - 写时复制:这是
CopyOnWriteArrayList
的核心机制。每当列表需要被修改时,并不是直接在原有数组上进行操作,而是先复制一个新数组,然后在副本上进行修改,最后将引用指向新的数组。 - 适用于读多写少的场景:由于每次写操作都要复制整个数组,所以如果写操作非常频繁,性能开销会很大。但是,在读取操作远多于写入操作的情况下,这种方式可以提供较好的性能,并且不需要对读取操作进行同步。
- 弱一致性:读操作可能读取到的是旧的数据,因为在某次写操作完成之前,其他线程看到的数据还是修改前的状态。
属性
在 CopyOnWriteArrayList
类中,定义了两个重要的属性,它们分别是 lock
和 array
。
lock
- 类型:
final transient Object
- 用途:
lock
是一个用于保护所有修改器(mutators)操作的内置监视器锁。在CopyOnWriteArrayList
中,所有的修改操作(如add
,set
,remove
等)都需要获取这个锁。这样做是为了确保当一个线程正在修改列表时,其他线程不能同时进行修改,从而保证线程安全性。 - 为什么使用内置监视器而不是
ReentrantLock
:注释中提到,当内置监视器(synchronized)和ReentrantLock
都可以满足需求时,作者有轻微的偏好使用内置监视器。这可能是因为内置监视器在大多数情况下性能足够,并且代码更简洁。
array
- 类型:
private transient volatile Object[]
- 用途:
array
是一个数组,它存储了CopyOnWriteArrayList
中的所有元素。这个数组只能通过getArray()
和setArray()
方法访问和修改。 - 关键字
volatile
:确保array
的读写操作对所有线程立即可见。这是因为在多线程环境下,一个线程对array
的修改需要被其他线程立即看到,以保证数据的一致性。 - 为什么使用
Object[]
而不是E[]
:在 Java 中,数组必须具有确切的类型,而泛型数组在 Java 中是不被直接支持的。因此,这里使用Object[]
作为基础类型来存储任意类型的元素。
主要方法
get
get
方法用于获取列表中指定索引位置的元素。以下是 get
方法的执行流程:
调用
get
方法:当外部代码调用CopyOnWriteArrayList
实例的get
方法,并传递一个索引index
作为参数时,执行流程开始。javapublic E get(int index) { return elementAt(getArray(), index); }
获取当前数组快照:
get
方法内部首先调用getArray()
方法。这个方法简单地返回了CopyOnWriteArrayList
的array
属性,这是一个包含所有元素的数组。javafinal Object[] getArray() { return array; }
在这个步骤中,我们得到了一个数组的引用,但请注意,由于
CopyOnWriteArrayList
的特性,这个数组是一个“快照”版本,不会在后续的迭代过程中发生变化。调用
elementAt
方法:接下来,get
方法将获得的数组a
和索引index
作为参数传递给elementAt
静态方法。javastatic <E> E elementAt(Object[] a, int index) { return (E) a[index]; }
elementAt
方法是一个泛型方法,它通过索引直接访问数组中的元素,并将其转型为泛型类型E
后返回。返回元素:
elementAt
方法返回数组中索引index
处的元素。这个元素被转型为泛型类型E
,并最终由get
方法返回给调用者。
在整个过程中,没有使用任何锁或同步机制,这是因为 CopyOnWriteArrayList
的设计确保了在读取时不需要同步。由于写操作会创建数组的新副本,所以读操作可以安全地访问当前数组,而不用担心其他线程的并发修改。
set
set
方法用于将列表中指定索引位置的元素替换为新元素。以下是 set
方法的执行流程:
同步块开始:方法开始时,通过
synchronized (lock)
语句获取lock
对象的监视器锁。这个锁确保了在同一时间只有一个线程能够执行set
方法中的代码,从而保证了线程安全性。javasynchronized (lock) { // ... }
获取当前数组快照:在同步块内部,首先调用
getArray()
方法获取当前数组的一个副本(快照)。javaObject[] es = getArray();
获取旧值:接着,调用
elementAt
方法获取指定索引index
处的元素,并将其存储在oldValue
变量中。javaE oldValue = elementAt(es, index);
检查是否需要替换:通过比较
oldValue
和要设置的新元素element
来判断是否需要进行替换。如果两者相同,则不需要任何操作。复制数组:如果
oldValue
和element
不同,那么需要复制当前数组,创建一个新的数组副本。javaes = es.clone();
设置新元素:在新复制的数组副本中,将指定索引
index
处的元素设置为新的元素element
。javaes[index] = element;
更新数组引用:通过调用
setArray
方法,将CopyOnWriteArrayList
的array
引用更新为新的数组副本。这确保了后续的读取操作能够看到最新的数组状态。javasetArray(es);
setArray
方法的实现如下:javafinal void setArray(Object[] a) { array = a; }
返回旧值:最后,方法返回之前在指定索引处存储的旧值
oldValue
。同步块结束:在同步块结束时,释放
lock
对象的监视器锁,允许其他线程进入同步块。
这种方法确保了在修改操作期间,其他线程对数组的读取操作不会受到影响,因为它们仍然可以访问原始数组,而修改操作是在数组的副本上进行的。
add
查看代码
public void add(int index, E element) {
synchronized (lock) {
Object[] es = getArray();
int len = es.length;
if (index > len || index < 0)
throw new IndexOutOfBoundsException(outOfBounds(index, len));
Object[] newElements;
int numMoved = len - index;
if (numMoved == 0)
newElements = Arrays.copyOf(es, len + 1);
else {
newElements = new Object[len + 1];
System.arraycopy(es, 0, newElements, 0, index);
System.arraycopy(es, index, newElements, index + 1,
numMoved);
}
newElements[index] = element;
setArray(newElements);
}
}
add
方法用于在列表的指定索引位置插入一个新元素。以下是 add
方法的执行流程:
同步块开始:方法开始时,通过
synchronized (lock)
语句获取lock
对象的监视器锁。这个锁确保了在同一时间只有一个线程能够执行add
方法中的代码,从而保证了线程安全性。javasynchronized (lock) { // ... }
获取当前数组快照:在同步块内部,首先调用
getArray()
方法获取当前数组的一个副本(快照)。javaObject[] es = getArray();
检查索引有效性:接着,计算当前数组的长度
len
,并检查指定的索引index
是否在数组的有效范围内(即大于等于 0 且小于等于数组的长度)。如果索引无效,则抛出IndexOutOfBoundsException
异常。javaint len = es.length; if (index > len || index < 0) throw new IndexOutOfBoundsException(outOfBounds(index, len));
创建新数组:根据索引位置,有两种情况处理方式:
如果索引等于数组的长度(即在数组末尾添加元素),则使用
Arrays.copyOf
方法创建一个新数组,长度比原数组多 1。javaif (numMoved == 0) newElements = Arrays.copyOf(es, len + 1);
如果索引小于数组的长度(即在数组中间插入元素),则创建一个新数组,长度同样比原数组多 1,并使用
System.arraycopy
方法将原数组中的元素复制到新数组中的正确位置。javaelse { newElements = new Object[len + 1]; System.arraycopy(es, 0, newElements, 0, index); System.arraycopy(es, index, newElements, index + 1, numMoved); }
设置新元素:在新创建的数组中,将指定索引
index
处的元素设置为新的元素element
。javanewElements[index] = element;
更新数组引用:通过调用
setArray
方法,将CopyOnWriteArrayList
的array
引用更新为新的数组副本。这确保了后续的读取操作能够看到最新的数组状态。javasetArray(newElements);
同步块结束:在同步块结束时,释放
lock
对象的监视器锁,允许其他线程进入同步块。
这个方法保证了即使在添加元素的过程中,其他线程的读取操作也不会受到影响,因为它们仍然可以访问原始数组。而所有修改操作都是在新的数组副本上进行的,这符合 CopyOnWriteArrayList
的写时复制(Copy-On-Write)机制。
add
末尾添加元素:
查看代码
public boolean add(E e) {
// 获取监视器锁,确保当前只有一个线程能够修改数组
synchronized (lock) {
// 获取当前数组的副本(快照)
Object[] es = getArray();
// 获取当前数组的长度
int len = es.length;
// 创建一个新的数组,长度为原数组长度加1
es = Arrays.copyOf(es, len + 1);
// 在新数组的末尾添加新元素e
es[len] = e;
// 更新数组引用,将array指向新的数组副本
setArray(es);
// 添加操作总是成功的,返回true
return true;
}
}
remove
移除指定索引位置的元素。
查看代码
public E remove(int index) {
// 获取监视器锁,确保当前只有一个线程能够修改数组
synchronized (lock) {
// 获取当前数组的副本(快照)
Object[] es = getArray();
// 获取当前数组的长度
int len = es.length;
// 获取指定索引处的元素,这将是要移除的元素的值
E oldValue = elementAt(es, index);
// 计算需要移动的元素数量,即从移除位置到数组末尾的元素数量
int numMoved = len - index - 1;
// 创建一个新的数组,长度为原数组长度减1
Object[] newElements;
// 如果没有元素需要移动(即移除的是最后一个元素),则直接复制原数组的前len-1个元素
if (numMoved == 0)
newElements = Arrays.copyOf(es, len - 1);
else {
// 否则,创建一个新的数组,并将原数组中除了要移除元素之外的其他元素复制到新数组中
newElements = new Object[len - 1];
// 复制索引之前的元素到新数组
System.arraycopy(es, 0, newElements, 0, index);
// 复制索引之后的元素到新数组,跳过要移除的元素
System.arraycopy(es, index + 1, newElements, index, numMoved);
}
// 更新数组引用,将array指向新的数组副本
setArray(newElements);
// 返回被移除元素的旧值
return oldValue;
}
}