引入
在 CopyOnWriteArrayList 出现之前,我们已经有了 ArrayList 和 LinkedList 作为 List 的数组和链表的实现,而且也有了线程安全的 Vector 和Collections.synchronizedList() 可以使用。
首先我们来看看Vector是如何实现线程安全的 ,还是老样子,先看看它的源码注释:
The Vector class implements a growable array of objects. Like an array, it contains components that can be accessed using an integer index. However, the size of a Vector can grow or shrink as needed to accommodate adding and removing items after the Vector has been created.
Each vector tries to optimize storage management by maintaining a capacity and a capacityIncrement. The capacity is always at least as large as the vector size; it is usually larger because as components are added to the vector, the vector's storage increases in chunks the size of capacityIncrement. An application can increase the capacity of a vector before inserting a large number of components; this reduces the amount of incremental reallocation.
The iterators returned by this class's iterator and listIterator methods are fail-fast: if the vector is structurally modified at any time after the iterator is created, in any way except through the iterator's own remove or add methods, the iterator will throw a ConcurrentModificationException. Thus, in the face of concurrent modification, the iterator fails quickly and cleanly, rather than risking arbitrary, non-deterministic behavior at an undetermined time in the future. The Enumerations returned by the elements method are not fail-fast.
Note that the fail-fast behavior of an iterator cannot be guaranteed as it is, generally speaking, impossible to make any hard guarantees in the presence of unsynchronized concurrent modification. Fail-fast iterators throw ConcurrentModificationException on a best-effort basis. Therefore, it would be wrong to write a program that depended on this exception for its correctness: the fail-fast behavior of iterators should be used only to detect bugs.
As of the Java 2 platform v1.2, this class was retrofitted to implement the List interface, making it a member of the Java Collections Framework. Unlike the new collection implementations, Vector is synchronized. If a thread-safe implementation is not needed, it is recommended to use ArrayList in place of Vector.翻译:
Vector 类实现了一个可增长的对象数组。就像一个数组一样,它包含可以通过整数索引访问的组件。然而,Vector 的大小可以根据需要添加或删除项目来增长或缩小。
每个 Vector 都通过维护容量和容量增量来优化存储管理。容量始终至少与 Vector 的大小一样大;它通常更大,因为当组件被添加到 Vector 中时,Vector 的存储会以容量增量大小的块增加。应用程序可以在插入大量组件之前增加 Vector 的容量,这可以减少增量重新分配的次数。
此类的 iterator 和 listIterator 方法返回的迭代器是快速失败的:如果在迭代器创建后,Vector 的结构被修改(除非通过迭代器自身的 remove 或 add 方法),迭代器将抛出 ConcurrentModificationException。因此,在面对并发修改时,迭代器会迅速且干净地失败,而不是冒着在未来某个不确定的时间出现任意、非确定性行为的风险。通过 elements 方法返回的枚举不是快速失败的。
请注意,迭代器的快速失败行为不能保证,因为在存在未同步的并发修改的情况下,通常不可能做出任何硬性保证。快速失败的迭代器会尽最大努力抛出 ConcurrentModificationException。因此,将程序的正确性依赖于此异常是错误的:迭代器的快速失败行为只能用于检测错误。
从 Java 2 平台 v1.2 开始,该类被改造为实现 List 接口,使其成为 Java Collections Framework 的一员。与新的集合实现不同,Vector 是同步的。如果不需要线程安全的实现,建议使用 ArrayList 代替 Vector。
下面我们看下它的 size 和 get方法的代码:
/*** 返回此向量中的组件数量。* * @return 此向量中的组件数量*/
public synchronized int size() {return elementCount;
}/*** 返回此向量中指定位置的元素。** @param index 要返回元素的索引* @return 指定索引处的对象* @throws ArrayIndexOutOfBoundsException 如果索引超出范围* ({@code index < 0 || index >= size()})* @since 1.2*/
public synchronized E get(int index) {// 检查索引是否超出向量的元素数量,若超出则抛出越界异常if (index >= elementCount)throw new ArrayIndexOutOfBoundsException(index);// 返回指定索引位置的元素return elementData(index);
}
可以看出,Vector 内部是使用 synchronized 来保证线程安全的,并且锁的粒度比较大,都是方法级别的锁,在并发量高的时候,很容易发生竞争,并发效率相对比较低。在这一点上,Vector 和 Hashtable很类似。并且它们在迭代期间都不允许编辑,如果在迭代期间进行添加或删除元素等操作,则会抛出 ConcurrentModificationException 异常,这样的特点也在很多情况下给使用者带来了麻烦。
所以从 JDK1.5 开始,Java 并发包里提供了使用 CopyOnWrite 机制实现的并发容器 CopyOnWriteArrayList 作为主要的并发 List,CopyOnWrite 的并发集合还包括CopyOnWriteArraySet,其底层正是利用 CopyOnWriteArrayList 实现的。
所以今天我们以CopyOnWriteArrayList 为突破口,来看一下 CopyOnWrite 容器的特点。
适用场景
读操作可以尽可能的快,而写即使慢一些也没关系
在很多应用场景中,读操作可能会远远多于写操作。比如,有些系统级别的信息,往往只需要加载或者修改很少的次数,但是会被系统内所有模块频繁的访问。对于这种场景,我们最希望看到的就是读操作可以尽可能的快,而写即使慢一些也没关系。
读多写少
黑名单是最典型的场景,假如我们有一个搜索网站,用户在这个网站的搜索框中,输入关键字搜索内容,但是某些关键字不允许被搜索。这些不能被搜索的关键字会被放在一个黑名单中,黑名单并不需要实时更新,可能每天晚上更新一次就可以了。当用户搜索时,会检查当前关键字在不在黑名单中,如果在,则提示不能搜索。这种读多写少的场景也很适合使用 CopyOnWrite 集合。
读写规则
读写锁的规则
读写锁的思想是:读读共享、其他都互斥(写写互斥、读写互斥、写读互斥),原因是由于读操作不会修改原有的数据,因此并发读并不会有安全问题;而写操作是危险的,所以当写操作发生时,不允许有读操作加入,也不允许第二个写线程加入。
对读写锁规则的升级
CopyOnWriteArrayList 的思想比读写锁的思想又更进一步。为了将读取的性能发挥到极致,CopyOnWriteArrayList 读取是完全不用加锁的,更厉害的是,写入也不会阻塞读取操作,也就是说你可以在写入的同时进行读取,只有写入和写入之间需要进行同步,也就是不允许多个写入同时发生,但是在写入发生时允许读取同时发生。这样一来,读操作的性能就会大幅度提升。
特点
CopyOnWrite的含义
从 CopyOnWriteArrayList 的名字就能看出它是满足 CopyOnWrite 的 ArrayList,CopyOnWrite 的意思是说,当容器需要被修改的时候,不直接修改当前容器,而是先将当前容器进行 Copy,复制出一个新的容器,然后修改新的容器,完成修改之后,再将原容器的引用指向新的容器。这样就完成了整个修改过程。
这样做的好处是,CopyOnWriteArrayList 利用了“不变性”原理,因为容器每次修改都是创建新副本,所以对于旧容器来说,其实是不可变的,也是线程安全的,无需进一步的同步操作。我们可以对 CopyOnWrite 容器进行并发的读,而不需要加锁,因为当前容器不会添加任何元素,也不会有修改。
CopyOnWriteArrayList 的所有修改操作(add,set等)都是通过创建底层数组的新副本来实现的,所以 CopyOnWrite 容器也是一种读写分离的思想体现,读和写使用不同的容器。
迭代期间允许修改集合内容
我们知道 ArrayList 在迭代期间如果修改集合的内容,会抛出 ConcurrentModificationException 异常。
让我们来分析一下 ArrayList 会抛出异常的原因,首先还是先看看它的源码注释:
Resizable-array implementation of the List interface. Implements all optional list operations, and permits all elements, including null. In addition to implementing the List interface, this class provides methods to manipulate the size of the array that is used internally to store the list. (This class is roughly equivalent to Vector, except that it is unsynchronized.)
The size, isEmpty, get, set, iterator, and listIterator operations run in constant time. The add operation runs in amortized constant time, that is, adding n elements requires O(n) time. All of the other operations run in linear time (roughly speaking). The constant factor is low compared to that for the LinkedList implementation.
Each ArrayList instance has a capacity. The capacity is the size of the array used to store the elements in the list. It is always at least as large as the list size. As elements are added to an ArrayList, its capacity grows automatically. The details of the growth policy are not specified beyond the fact that adding an element has constant amortized time cost.
An application can increase the capacity of an ArrayList instance before adding a large number of elements using the ensureCapacity operation. This may reduce the amount of incremental reallocation.
Note that this implementation is not synchronized. If multiple threads access an ArrayList instance concurrently, and at least one of the threads modifies the list structurally, it must be synchronized externally. (A structural modification is any operation that adds or deletes one or more elements, or explicitly resizes the backing array; merely setting the value of an element is not a structural modification.) This is typically accomplished by synchronizing on some object that naturally encapsulates the list. If no such object exists, the list should be "wrapped" using the Collections. synchronizedList method. This is best done at creation time, to prevent accidental unsynchronized access to the list:
List list = Collections. synchronizedList(new ArrayList(...));
The iterators returned by this class's iterator and listIterator methods are fail-fast: if the list is structurally modified at any time after the iterator is created, in any way except through the iterator's own remove or add methods, the iterator will throw a ConcurrentModificationException. Thus, in the face of concurrent modification, the iterator fails quickly and cleanly, rather than risking arbitrary, non-deterministic behavior at an undetermined time in the future.
Note that the fail-fast behavior of an iterator cannot be guaranteed as it is, generally speaking, impossible to make any hard guarantees in the presence of unsynchronized concurrent modification. Fail-fast iterators throw ConcurrentModificationException on a best-effort basis. Therefore, it would be wrong to write a program that depended on this exception for its correctness: the fail-fast behavior of iterators should be used only to detect bugs.翻译:
List 接口的可调整大小数组实现。实现了所有可选的列表操作,并允许所有元素,包括 null。除了实现 List 接口外,此类还提供了用于操作内部存储列表的数组大小的方法。(这个类大致等同于 Vector,但它是不同步的。)
大小、isEmpty、get、set、iterator 和 listIterator 操作在常数时间内运行。add 操作以分摊常数时间运行,也就是说,添加 n 个元素需要 O(n) 时间。所有其他操作大致以线性时间运行。与 LinkedList 实现相比,这个类的常数因子较低。
每个 ArrayList 实例都有一个容量。容量是用于存储列表中元素的数组的大小。它始终至少与列表大小一样大。当元素被添加到 ArrayList 中时,其容量会自动增长。增长策略的细节没有具体规定,只知道添加元素的分摊时间成本是常数。
可以在添加大量元素之前使用 ensureCapacity 操作来增加 ArrayList 实例的容量。这可能会减少增量重新分配的次数。
请注意,这个实现不是同步的。如果多个线程同时访问 ArrayList 实例,且至少有一个线程在结构上修改了列表,那么必须在外部进行同步。(结构上的修改是指添加或删除一个或多个元素,或者显式调整底层数组的大小;仅设置元素的值不是结构上的修改。)这通常通过在自然封装列表的对象上进行同步来实现。如果没有这样的对象,应使用 Collections.synchronizedList 方法将列表 “包装” 起来。最好在创建时就进行此操作,以防止列表被意外地未同步访问:
List list = Collections.synchronizedList(new ArrayList(...));
此类的 iterator 和 listIterator 方法返回的迭代器是快速失败的:如果在迭代器创建后,列表的结构被修改(除非通过迭代器自身的 remove 或 add 方法),迭代器将抛出 ConcurrentModificationException。因此,在面对并发修改时,迭代器会迅速且干净地失败,而不是冒着在未来某个不确定的时间出现任意、非确定性行为的风险。
请注意,迭代器的快速失败行为不能保证,因为在存在未同步的并发修改的情况下,通常不可能做出任何硬性保证。快速失败的迭代器会尽最大努力抛出 ConcurrentModificationException。因此,将程序的正确性依赖于此异常是错误的:迭代器的快速失败行为只能用于检测错误。
下面我们重点看其中在 ArrayList 源码里的 Itr 类,代码如下:
/*** 用于迭代 ArrayList 中的元素。* 这个迭代器是 fail-fast 的,意味着在创建迭代器之后,如果 ArrayList 的结构发生了改变,* 迭代器会抛出 ConcurrentModificationException 异常。*/private class Itr implements Iterator<E> {// 下一个要返回的元素的索引int cursor; // 最后一个返回的元素的索引;如果没有则为 -1int lastRet = -1; // 期望的修改计数,用于检测并发修改int expectedModCount = modCount;/*** 检查是否还有下一个元素。** @return 如果还有下一个元素则返回 true,否则返回 false*/public boolean hasNext() {return cursor != size;}/*** 返回迭代器的下一个元素,并将迭代器的位置向后移动一位。** @return 迭代器的下一个元素* @throws NoSuchElementException 如果没有更多的元素* @throws ConcurrentModificationException 如果在迭代过程中 ArrayList 的结构发生了改变*/@SuppressWarnings("unchecked")public E next() {// 检查是否在迭代过程中发生了并发修改checkForComodification();int i = cursor;if (i >= size)throw new NoSuchElementException();Object[] elementData = ArrayList.this.elementData;if (i >= elementData.length)throw new ConcurrentModificationException();cursor = i + 1;return (E) elementData[lastRet = i];}/*** 移除迭代器最后返回的元素。* 这个方法只能在每次调用 next() 方法之后调用一次。** @throws IllegalStateException 如果 lastRet 小于 0,意味着还没有调用过 next() 方法或者已经调用过 remove() 方法* @throws ConcurrentModificationException 如果在迭代过程中 ArrayList 的结构发生了改变*/public void remove() {if (lastRet < 0)throw new IllegalStateException();// 检查是否在迭代过程中发生了并发修改checkForComodification();try {// 调用 ArrayList 的 remove 方法移除最后返回的元素ArrayList.this.remove(lastRet);cursor = lastRet;lastRet = -1;expectedModCount = modCount;} catch (IndexOutOfBoundsException ex) {throw new ConcurrentModificationException();}}/*** 对迭代器中剩余的所有元素执行给定的操作。** @param consumer 要对每个剩余元素执行的操作* @throws NullPointerException 如果 consumer 为 null* @throws ConcurrentModificationException 如果在迭代过程中 ArrayList 的结构发生了改变*/@Override@SuppressWarnings("unchecked")public void forEachRemaining(Consumer<? super E> consumer) {// 确保 consumer 不为 nullObjects.requireNonNull(consumer);final int size = ArrayList.this.size;int i = cursor;if (i >= size) {return;}final Object[] elementData = ArrayList.this.elementData;if (i >= elementData.length) {throw new ConcurrentModificationException();}// 对剩余元素执行 consumer 操作while (i != size && modCount == expectedModCount) {consumer.accept((E) elementData[i++]);}// 在迭代结束时更新 cursor 和 lastRet,以减少堆写入流量cursor = i;lastRet = i - 1;// 检查是否在迭代过程中发生了并发修改checkForComodification();}/*** 检查在迭代过程中 ArrayList 的结构是否发生了改变。* 如果 modCount 不等于 expectedModCount,则抛出 ConcurrentModificationException 异常。** @throws ConcurrentModificationException 如果在迭代过程中 ArrayList 的结构发生了改变*/final void checkForComodification() {if (modCount != expectedModCount)throw new ConcurrentModificationException();}}
其中 checkForComodification 方法,里会首先检查 modCount 是否等于 expectedModCount。modCount 是保存修改次数,每次我们调用 add、remove 或 trimToSize 等方法时它会增加,expectedModCount 是迭代器的变量,当我们创建迭代器时会初始化并记录当时的 modCount。后面迭代期间如果发现 modCount 和 expectedModCount 不一致,就说明有人修改了集合的内容,就会抛出异常。
和 ArrayList 不同的是,CopyOnWriteArrayList 的迭代器在迭代的时候,如果数组内容被修改了,
CopyOnWriteArrayList 不会报 ConcurrentModificationException 的异常,因为迭代器使用的依然是旧数组,只不过迭代的内容可能已经过时了。
CopyOnWriteArrayList 的迭代器一旦被建立之后,如果往之前的 CopyOnWriteArrayList 对象中去新增元素,在迭代器中既不会显示出元素的变更情况,同时也不会报错,这一点和 ArrayList 是有很大区别的。
缺点
这些缺点不仅是针对 CopyOnWriteArrayList,其实同样也适用于其他的 CopyOnWrite 容器:
内存占用问题
因为 CopyOnWrite 的写时复制机制,所以在进行写操作的时候,内存里会同时驻扎两个对象的内存,这一点会占用额外的内存空间。
在元素较多或者复杂的情况下,复制的开销很大复制过程不仅会占用双倍内存,还需要消耗 CPU 等资源,会降低整体性能。
数据一致性问题
由于 CopyOnWrite 容器的修改是先修改副本,所以这次修改对于其他线程来说,并不是实时能看到的,只有在修改完之后才能体现出来。如果你希望写入的的数据马上能被其他线程看到,CopyOnWrite 容器是不适用的。
源码分析
源码注释
还是老样子,先看看源码注释:
A thread-safe variant of java. util. ArrayList in which all mutative operations (add, set, and so on) are implemented by making a fresh copy of the underlying array.
This is ordinarily too costly, but may be more efficient than alternatives when traversal operations vastly outnumber mutations, and is useful when you cannot or don't want to synchronize traversals, yet need to preclude interference among concurrent threads. The "snapshot" style iterator method uses a reference to the state of the array at the point that the iterator was created. This array never changes during the lifetime of the iterator, so interference is impossible and the iterator is guaranteed not to throw ConcurrentModificationException. The iterator will not reflect additions, removals, or changes to the list since the iterator was created. Element-changing operations on iterators themselves (remove, set, and add) are not supported. These methods throw UnsupportedOperationException.
All elements are permitted, including null.
Memory consistency effects: As with other concurrent collections, actions in a thread prior to placing an object into a CopyOnWriteArrayList happen-before actions subsequent to the access or removal of that element from the CopyOnWriteArrayList in another thread.
This class is a member of the Java Collections Framework.翻译:
这是 java.util.ArrayList 的一种线程安全变体,其中所有修改操作(如添加、设置等)都是通过制作底层数组的新副本实现的。
这通常成本较高,但在遍历操作远多于修改操作的情况下,可能比其他替代方案更高效。在不能或不想同步遍历时,却又需要防止并发线程之间的干扰时,这种实现方式非常有用。“快照”风格的迭代器方法使用的是迭代器创建时数组状态的引用。此数组在迭代器的生命周期内永远不会改变,因此不可能发生干扰,迭代器也保证不会抛出 ConcurrentModificationException。迭代器不会反映自创建以来列表的添加、删除或更改。迭代器自身的元素更改操作(如 remove、set 和 add)不受支持,这些方法会抛出 UnsupportedOperationException。
所有元素都是允许的,包括 null。
内存一致性效果:与其他并发集合一样,将对象放入 CopyOnWriteArrayList 之前的线程操作,在另一个线程访问或移除该元素之后的操作之前发生。
此类是 Java 集合框架的成员。
数据结构
/*** 用于保护所有修改操作的锁。(可重入锁)* 由于修改操作需要创建底层数组的新副本,因此使用锁来确保线程安全。*/final transient ReentrantLock lock = new ReentrantLock();/*** 存储列表元素的数组。* 该数组只能通过 getArray 和 setArray 方法访问。* 使用 transient 关键字表示该字段不会被序列化。* 使用 volatile 关键字确保数组的修改对其他线程可见。*/private transient volatile Object[] array;/*** 获取当前存储列表元素的数组。* 此方法非私有,以便 CopyOnWriteArraySet 类也能访问。** @return 当前存储列表元素的数组*/final Object[] getArray() {return array;}/*** 设置存储列表元素的数组。** @param a 要设置的新数组*/final void setArray(Object[] a) {array = a;}/*** 构造一个空的 CopyOnWriteArrayList。* 初始化底层数组为空数组。*/public CopyOnWriteArrayList() {setArray(new Object[0]);}
在这个类中首先会有一个 ReentrantLock 锁,用来保证修改操作的线程安全。下面被命名为 array 的 Object[] 数组是被 volatile 修饰的,可以保证数组的可见性,这正是存储元素的数组,同样我们可以从 getArray()、setArray 以及它的构造方法看出,CopyOnWriteArrayList 的底层正是利用数组实现的,这也符合它的名字。
add 方法
/*** 向列表末尾添加指定元素。** 该方法会获取锁以确保线程安全,创建一个新数组,该数组长度比原数组大1,* 并将原数组元素复制到新数组中,然后将指定元素添加到新数组的末尾,* 最后更新列表的内部数组。** @param e 要添加到列表的元素* @return 始终返回 {@code true},表示元素添加成功*/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);// 返回true表示添加成功return true;} finally {// 释放锁lock.unlock();}}
add 方法的作用是往 CopyOnWriteArrayList 中添加元素,是一种修改操作。首先需要利用ReentrantLock 的 lock 方法进行加锁,获取锁之后,得到原数组的长度和元素,也就是利用 getArray 方法得到 elements 并且保存 length。之后利用 Arrays.copyOf 方法复制出一个新的数组,得到一个和原数组内容相同的新数组,并且把新元素添加到新数组中。完成添加动作后,需要转换引用所指向的对象,利用 setArray(newElements) 操作就可以把 volatile Object[] array 的指向替换成新数组,最后在finally 中把锁解除。
总结流程:在添加的时候首先上锁,并复制一个新数组,增加操作在新数组上完成,然后将 array 指向到新数组,最后解锁。
上面的步骤实现了 CopyOnWrite 的思想:写操作是在原来容器的拷贝上进行的,并且在读取数据的时候不会锁住 list。而且可以看到,如果对容器拷贝操作的过程中有新的读线程进来,那么读到的还是旧的数据,因为在那个时候对象的引用还没有被更改。
下面我们来分析一下读操作的代码,也就是和 get 相关的三个方法,分别是 get 方法的两个重载和
getArray 方法,代码如下:
/*** 从指定的数组中获取指定索引位置的元素。* 此方法是一个辅助方法,用于从数组中获取元素并进行类型转换。** @param a 要从中获取元素的数组* @param index 要获取元素的索引位置* @return 指定索引位置的元素* @throws ArrayIndexOutOfBoundsException 如果索引超出数组的有效范围*/private E get(Object[] a, int index) {return (E) a[index];}/*** 获取列表中指定索引位置的元素。* 此方法调用内部的get(Object[] a, int index)方法,从列表的底层数组中获取元素。** @param index 要获取元素的索引位置* @return 指定索引位置的元素* @throws IndexOutOfBoundsException 如果索引超出列表的有效范围*/public E get(int index) {return get(getArray(), index);}/*** 获取当前存储元素的数组。* 此方法并非私有方法,以便 CopyOnWriteArraySet 类也能访问该数组。* * @return 存储元素的数组*/final Object[] getArray() {return array;}
可以看出,get 相关的操作没有加锁,保证了读取操作的高速。
迭代器 COWIterator 类
/*** 一个实现了 ListIterator 接口的迭代器类,用于 CopyOnWriteArrayList。* 该迭代器提供了列表元素的快照,在迭代过程中不会反映列表的并发修改。* 不支持 remove、set 和 add 操作,因为这些操作会改变列表的结构,* 而此迭代器基于列表的快照,不允许在迭代时修改列表。** @param <E> 迭代器要遍历的元素类型*/static final class COWIterator<E> implements ListIterator<E> {/*** 列表元素的快照数组。* 该数组在迭代器创建时被初始化,反映了当时列表的状态。*/private final Object[] snapshot;/*** 下一次调用 next() 方法时要返回的元素的索引。* 初始值可以通过构造函数指定。*/private int cursor;/*** 构造一个新的 COWIterator,使用给定的数组元素和初始游标位置。** @param elements 列表元素的快照数组* @param initialCursor 初始游标位置,指示迭代开始的位置*/private COWIterator(Object[] elements, int initialCursor) {// 初始化游标为初始游标位置cursor = initialCursor;// 保存列表元素的快照数组snapshot = elements;}/*** 检查迭代器是否还有下一个元素。** @return 如果游标小于快照数组的长度,则返回 true;否则返回 false*/public boolean hasNext() {// 通过比较游标和快照数组长度判断是否有下一个元素return cursor < snapshot.length;}/*** 检查迭代器是否还有前一个元素。** @return 如果游标大于 0,则返回 true;否则返回 false*/public boolean hasPrevious() {// 通过判断游标是否大于 0 来确定是否有前一个元素return cursor > 0;}/*** 返回迭代器的下一个元素,并将游标向前移动一位。** @return 迭代器的下一个元素* @throws NoSuchElementException 如果没有下一个元素*/@SuppressWarnings("unchecked")public E next() {// 检查是否有下一个元素if (! hasNext())// 若没有则抛出异常throw new NoSuchElementException();// 返回当前游标位置的元素,并将游标后移一位return (E) snapshot[cursor++];}/*** 返回迭代器的前一个元素,并将游标向后移动一位。** @return 迭代器的前一个元素* @throws NoSuchElementException 如果没有前一个元素*/@SuppressWarnings("unchecked")public E previous() {// 检查是否有前一个元素if (! hasPrevious())// 若没有则抛出异常throw new NoSuchElementException();// 返回前一个元素,并将游标前移一位return (E) snapshot[--cursor];}/*** 返回调用 next() 方法时将返回的元素的索引。** @return 下一个元素的索引*/public int nextIndex() {// 返回当前游标位置return cursor;}/*** 返回调用 previous() 方法时将返回的元素的索引。** @return 前一个元素的索引*/public int previousIndex() {// 返回游标前一个位置的索引return cursor-1;}/*** 此迭代器不支持 remove 操作。** @throws UnsupportedOperationException 总是抛出此异常,因为该迭代器不支持 remove 操作*/public void remove() {// 抛出不支持操作的异常throw new UnsupportedOperationException();}/*** 此迭代器不支持 set 操作。** @param e 要设置的元素* @throws UnsupportedOperationException 总是抛出此异常,因为该迭代器不支持 set 操作*/public void set(E e) {// 抛出不支持操作的异常throw new UnsupportedOperationException();}/*** 此迭代器不支持 add 操作。** @param e 要添加的元素* @throws UnsupportedOperationException 总是抛出此异常,因为该迭代器不支持 add 操作*/public void add(E e) {// 抛出不支持操作的异常throw new UnsupportedOperationException();}/*** 对迭代器中剩余的每个元素执行给定的操作,直到所有元素都被处理或操作抛出异常。** @param action 要对每个元素执行的操作* @throws NullPointerException 如果指定的操作为 null*/@Overridepublic void forEachRemaining(Consumer<? super E> action) {// 检查操作是否为 nullObjects.requireNonNull(action);// 获取快照数组Object[] elements = snapshot;// 获取数组长度final int size = elements.length;// 从当前游标位置开始遍历数组for (int i = cursor; i < size; i++) {// 将元素转换为泛型类型@SuppressWarnings("unchecked") E e = (E) elements[i];// 对元素执行操作action.accept(e);}// 将游标设置为数组长度,表示迭代结束cursor = size;}}
这个迭代器有两个重要的属性,分别是 Object[] snapshot 和 int cursor。其中 snapshot 代表数组的快照,也就是创建迭代器那个时刻的数组情况,而 cursor 则是迭代器的游标。
迭代器在被构建的时候,会把当时的 elements 赋值给 snapshot,而之后的迭代器所有的操作都基于 snapshot 数组进行的。
在 next 方法中可以看到,返回的内容是 snapshot 对象,所以,后续就算原数组被修改,这个snapshot 既不会感知到,也不会受影响,执行迭代操作不需要加锁,也不会因此抛出异常。迭代器返回的结果,和创建迭代器的时候的内容一致。
总结
我们对 CopyOnWriteArrayList 进行了介绍。我们分别介绍了在它诞生之前的 Vector 和
Collections.synchronizedList() 的特点,CopyOnWriteArrayList 的适用场景、读写规则,还介绍了它的两个特点,分别是写时复制和迭代期间允许修改集合内容。我们还介绍了它的三个缺点,分别是内存占用问题,在元素较多或者复杂的情况下复制的开销大问题,以及数据一致性问题。最后我们对于它的重要源码进行了解析。