当前位置: 首页 > news >正文

Java多线程与高并发专题——关于CopyOnWrite 容器特点

引入

在 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 的适用场景、读写规则,还介绍了它的两个特点,分别是写时复制和迭代期间允许修改集合内容。我们还介绍了它的三个缺点,分别是内存占用问题,在元素较多或者复杂的情况下复制的开销大问题,以及数据一致性问题。最后我们对于它的重要源码进行了解析。

相关文章:

Java多线程与高并发专题——关于CopyOnWrite 容器特点

引入 在 CopyOnWriteArrayList 出现之前&#xff0c;我们已经有了 ArrayList 和 LinkedList 作为 List 的数组和链表的实现&#xff0c;而且也有了线程安全的 Vector 和Collections.synchronizedList() 可以使用。 首先我们来看看Vector是如何实现线程安全的 &#xff0c;还是…...

Wpf-ReactiveUI-Usercontrol与主界面交互

文章目录 Usercontrol与主界面方法一、使用属性绑定UserControl 部分(MyUserControl.xaml.cs)UserControl 视图模型部分(MyUserControlViewModel.cs)主界面部分(MainWindow.xaml)主界面视图模型部分(MainWindowViewModel.cs)方法二、使用消息传递UserControl 视图模型部…...

MySQL中like模糊查询如何优化?

大家好&#xff0c;我是锋哥。今天分享关于【MySQL中like模糊查询如何优化?】面试题。希望对大家有帮助&#xff1b; MySQL中like模糊查询如何优化? 1000道 互联网大厂Java工程师 精选面试题-Java资源分享网 在 MySQL 中&#xff0c;LIKE 模糊查询虽然非常常见&#xff0c;…...

用Ruby的Faraday库来进行网络请求抓取数据

在 Ruby 中&#xff0c;Faraday 是一个非常强大的 HTTP 客户端库&#xff0c;它可以用于发送 HTTP 请求并处理响应。你可以使用 Faraday 来抓取网页数据&#xff0c;处理 API 请求等任务。下面我将向你展示如何使用 Faraday 库进行网络请求&#xff0c;抓取数据并处理响应。 1.…...

2025天梯训练1

PTA | L3-1 直捣黄龙 30分 思路&#xff1a;多关键字最短路&#xff0c;同时还要记录最短路径条数。 typedef struct node{int from,d,pass,kl;bool operator<(const node &x)const{if(d!x.d) return d>x.d;if(pass!x.pass) return pass<x.pass;return kl<x.…...

DeepSeek教我写词典爬虫获取单词的音标和拼写

Python在爬虫领域展现出了卓越的功能性&#xff0c;不仅能够高效地抓取目标数据&#xff0c;还能便捷地将数据存储至本地。在众多Python爬虫应用中&#xff0c;词典数据的爬取尤为常见。接下来&#xff0c;我们将以dict.cn为例&#xff0c;详细演示如何编写一个用于爬取词典数据…...

揭开AI-OPS 的神秘面纱 第四讲 AI 模型服务层(自研方向)

AI 模型服务层技术架构与组件选型分析(自研方向) 基于自有开发寻训练方向 AI 模型服务层 是 AI-Ops 架构的 核心智能引擎,负责构建、训练、部署、管理和监控各种 AI 模型,为上层应用服务层提供智能分析和决策能力。 AI 模型服务层需要提供一个灵活、可扩展、高性能的平台…...

[通俗易懂C++]:std::optional

[通俗易懂C]:std::optional 考虑下面这样一个函数: int doIntDivision(int x, int y) {return x / y; }如果调用者传入一个语义上无效的值&#xff08;例如 y 0 &#xff09;&#xff0c;此函数无法计算一个返回值&#xff08;因为除以 0 在数学上是未定义的&#xff09;。在…...

深入理解与配置 Nginx TCP 日志输出

一、背景介绍 在现代网络架构中&#xff0c;Nginx 作为一款高性能的 Web 服务器和反向代理服务器&#xff0c;广泛应用于各种场景。除了对 HTTP/HTTPS 协议的出色支持&#xff0c;Nginx 从 1.9.0 版本开始引入了对 TCP 和 UDP 协议的代理功能&#xff0c;这使得它在处理数据库…...

使用 vxe-table 导出 excel,支持带数值、货币、图片等带格式导出

使用 vxe-table 导出 excel&#xff0c;支持带数值、货币、图片等带格式导出&#xff0c;通过官方自动的导出插件 plugin-export-xlsx 实现导出功能 查看官网&#xff1a;https://vxetable.cn gitbub&#xff1a;https://github.com/x-extends/vxe-table gitee&#xff1a;htt…...

Spring-事务

Spring 事务 事务的基本概念 &#x1f539; 什么是事务&#xff1f; 事务是一组数据库操作&#xff0c;它们作为一个整体&#xff0c;要么全部成功&#xff0c;要么全部回滚。 常见的事务场景&#xff1a; 银行转账&#xff08;扣款和存款必须同时成功&#xff09; 订单系统…...

开源项目介绍:Native-LLM-for-Android

项目地址&#xff1a;Native-LLM-for-Android 创作活动时间&#xff1a;2025年 支持在 Android 设备上运行大型语言模型 &#xff08;LLM&#xff09; &#xff0c;具体支持的模型包括&#xff1a; DeepSeek-R1-Distill-Qwen: 1.5B Qwen2.5-Instruct: 0.5B, 1.5B Qwen2/2.5VL:…...

vocabulary is from your listening,other speaking and your thought.

不要把单词放在自己的大脑里&#xff0c;放在自己的嘴巴里&#xff0c;自己在那疯狂重复的recite&#xff0c;its futile.只是单点记忆单词&#xff0c;记住也是temporary&#xff0c;而且是单点的记忆&#xff0c;当别人说此词汇&#xff0c;你也听不懂或分辨就是这个单词&…...

前端知识点---路由模式-实例模式和单例模式(ts)

在 ArkTS&#xff08;Ark UI 框架&#xff09;中&#xff0c;路由实例模式&#xff08;Standard Instance Mode&#xff09;主要用于管理页面跳转。当创建一个新页面时&#xff0c;可以选择标准实例模式&#xff08;Standard Mode&#xff09;或单实例模式&#xff08;Single M…...

Matplotlib库中color 参数颜色有多少种

**当我们绘图超过十几种颜色。我都是 定义颜色列表&#xff08; ** colors [blue, green, red, cyan, magenta, yellow, greenyellow, orange, purple, brown, pink, gray]1、 颜色名称 Matplotlib常用的颜色名称&#xff1a; red 或 r green 或 g blue 或 b yellow 或 y cy…...

LINUX虚拟机中,不能播放RTSP设备,怎么办

首先&#xff0c;主机能够打开RTSP设备。虚拟机无法打开。 虚拟机网络设置 桥接网卡&#xff0c;选择正确的网卡。 虚拟机IP设置 需要是同一个网段。交换机的设备是192.168.1.192 用ffplay测试...

宝塔的ssl文件验证域名后,会在域名解析列表中留下记录吗?

在使用宝塔面板进行SSL证书验证域名后&#xff0c;通常不会在域名解析列表中留下记录。验证过程中添加的TXT记录仅用于验证域名的所有权&#xff0c;一旦验证完成&#xff0c;就可以安全地删除这些记录&#xff0c;不会影响SSL证书的正常使用。根据搜索结果&#xff0c;DNS验证…...

mitt 依赖库详解

一、概述 mitt 是一个极其轻量级的 JavaScript 事件发射器库&#xff0c;实现了发布-订阅模式。该模式允许对象间松散耦合&#xff0c;一个对象&#xff08;发布者&#xff09;可以发布事件&#xff0c;而其他对象&#xff08;订阅者&#xff09;可以监听这些事件并作出响应。…...

OSPF网络类型:NBMA与P2MP

一、NBMA网络 NBMA网络的特点 连接方式&#xff1a; 支持多台设备连接到同一个网络段&#xff0c;但网络本身不支持广播或组播。典型例子&#xff1a;帧中继、ATM。 DR/BDR选举&#xff1a; 由于网络不支持广播&#xff0c;OSPF需要手动配置邻居。 仍然会选举DR&#xff08…...

VUE叉的工作原理?

Vuex 是 Vue.js 的一个专门用于状态管理的库&#xff0c;其工作原理基于单向数据流和集中式存储&#xff0c;旨在解决跨组件之间状态共享和管理的复杂性。以下是 Vuex 的核心工作原理及其关键组成部分&#xff1a; 核心概念 State&#xff08;状态&#xff09; Vuex 使用一个单…...

H.264语法结构分析之frame_cropping_flag

技术背景 开发者对接我们轻量级RTSP服务的时候&#xff0c;利用部分开源的播放器&#xff0c;播放流数据的时候&#xff0c;说分辨率不对&#xff0c;导致看到的图像有问题&#xff0c;用我们的player或常见的通用播放器&#xff0c;又都是好的&#xff0c;这就扯到了今天的主…...

【智能体】本地安装Conda和搭建OpenManus环境

整理不易&#xff0c;请不要令色你的赞和收藏。 1. 前言 没体验到 Manus&#xff1f;不妨使用 MetaGPT 开源的 OpenManus 搭建自己的 Manus。本文将带你安装 Conda 并搭建自己的 Manus。 2. 前提条件 已安装 conda&#xff0c;没安装的话&#xff0c;下文有介绍。 OpenManu…...

【linux网络编程】套接字编程API详细介绍

在C语言中&#xff0c;套接字&#xff08;Socket&#xff09;编程主要用于网络通信&#xff0c;尤其是在基于TCP/IP协议的应用程序开发中。常用的套接字编程API主要基于Berkeley Sockets&#xff08;伯克利套接字&#xff09;接口&#xff0c;这些函数通常在<sys/socket.h&g…...

杂项知识笔记搜集

1.pygame pygame可以画出来图形界面&#xff0c;pygame Python仓库 PyGame游戏编程_游戏程序设计csdn-CSDN博客 2.V4L2库 V4L2是Linux上的Camera采集器的框架 Video for Linux &#xff0c;是从Linux2.1版本开始支持的。HDMI视频采集卡采集到的视频通过USB3.0输出&#xff0…...

模型微调——模型性能提升方法及注意事项(自用)

名词补充 人为为训练数据标注的标签称为黄金标准或真实值&#xff0c;这个过程一定程度上保证训练的准确性&#xff0c;但是其人工标注的成本和时间很高&#xff0c;并且标注的标签受人的主观因素影响。 导致模型性能不佳的因素和解决办法 ①不同类别的数据不平衡&#xff1a;统…...

RabbitMQ之旅(1)

相信自己,终会成功 目录 主流MQ产品 1.kafaka 2.RocketMQ 3.RabbitMQ 在xshell上安装RabbitMQ RabbitMQ七种工作模式 1.简单模式 ​编辑 2.工作队列模式 3.发布/订阅模式 4.路由模式 5.通配符模式 6.RPC模式 AMQP.BasicProperties 设置消息属性的类 7.发布确认模…...

TCP7680端口是什么服务

WAF上看到有好多tcp7680端口的访问信息 于是上网搜索了一下&#xff0c;确认TCP7680端口是Windows系统更新“传递优化”功能的服务端口&#xff0c;个人理解应该是Windows利用这个TCP7680端口&#xff0c;直接从内网已经具备更新包的主机上共享下载该升级包&#xff0c;无需从微…...

跟着 Lua 5.1 官方参考文档学习 Lua (11)

文章目录 5.4.1 – PatternsCharacter Class:Pattern Item:Pattern:Captures: string.find (s, pattern [, init [, plain]])例子&#xff1a;string.find 的简单使用 string.match (s, pattern [, init])string.gmatch (s, pattern)string.gsub (s, pattern, repl [, n])例子&…...

<script setup>和export default { setup() { ... } }区别

在 Vue 3 组合式 API&#xff08;Composition API&#xff09;中&#xff0c;<script setup> 和 export default setup() {} 都用于定义组件的逻辑&#xff0c;但它们有一些重要的区别&#xff1a; 1️⃣ <script setup>&#xff08;推荐&#xff09; ✅ 更简洁、…...

leetcode hot100--动态规划【五步总纲】

五步&#xff1a; 1.dp数组以及下标定义 dp[i] 2.递推公式 dp[n]dp[n-1]dp[n-2] 3.dp数组如何初始化 注意&#xff1a;判断边界条件&#xff0c;n0dp[1]就不存在【斐波那契】 4.遍历顺序 for循环顺序 5.打印数组【debug】 第一题&#xff1a;斐波那契数列 首先回顾了…...

RtlLookupAtomInAtomTable函数分析之RtlpAtomMapAtomToHandleEntry函数的作用是验证其正确性

第一部分&#xff1a; NTSTATUS RtlLookupAtomInAtomTable( IN PVOID AtomTableHandle, IN PWSTR AtomName, OUT PRTL_ATOM Atom OPTIONAL ) { NTSTATUS Status; PRTL_ATOM_TABLE p (PRTL_ATOM_TABLE)AtomTableHandle; PRTL_ATOM_TABLE_ENTRY a; …...

【从零开始学习计算机科学】硬件设计与FPGA原理

硬件设计 硬件设计流程 在设计硬件电路之前,首先要把大的框架和架构要搞清楚,这要求我们搞清楚要实现什么功能,然后找找有否能实现同样或相似功能的参考电路板(要懂得尽量利用他人的成果,越是有经验的工程师越会懂得借鉴他人的成果)。如果你找到了的参考设计,最好还是…...

todo: 使用融云imserve做登录(android)

使用融云做登录注册思路 注册界面需要name, email, password考虑到融云注册用户的post格式 POST http://api.rong-api.com/user/getToken.json?userId1690544550qqcom&nameIronman这里的userId可以使用用户的email&#xff0c;但是要截断和 . 符号&#xff0c;即1690544…...

从0开始的操作系统手搓教程23:构建输入子系统——实现键盘驱动1——热身驱动

目录 所以&#xff0c;键盘是如何工作的 说一说我们的8042 输出缓冲区寄存器 状态寄存器 控制寄存器 动手&#xff01; 注册中断 简单整个键盘驱动 Reference ScanCode Table 我们下一步就是准备进一步完善我们系统的交互性。基于这个&#xff0c;我们想到的第一个可以…...

Azure云生态系统详解:核心服务、混合架构与云原生概念

核心服务&#xff1a;深入掌握Azure SQL Database、Azure Database for PostgreSQL、Azure Database for MySQL的架构、备份恢复、高可用性配置&#xff08;如Geo-Replication、自动故障转移组、异地冗余备份&#xff09;。混合架构&#xff1a;熟悉Azure Arc&#xff08;管理混…...

Unity Dots

文章目录 什么是DotsDOTS的优势ECS&#xff08;实体组件系统&#xff09;Job System作业系统Burst编译器最后 什么是Dots DOTS&#xff08;Data-Oriented Technology Stack&#xff09;是Unity推出的一种用于开发高性能游戏和应用的数据导向技术栈&#xff0c;包含三大核心组件…...

SAP DOI EXCEL宏的使用

OAOR里上传EXCEL模版 屏幕初始化PBO创建DOI EXCEL对象&#xff0c;并填充EXCEL内容 *&---------------------------------------------------------------------* *& Module INIT_DOI_DISPLAY_9100 OUTPUT *&--------------------------------------------…...

VSTO(C#)Excel开发3:Range对象 处理列宽和行高

初级代码游戏的专栏介绍与文章目录-CSDN博客 我的github&#xff1a;codetoys&#xff0c;所有代码都将会位于ctfc库中。已经放入库中我会指出在库中的位置。 这些代码大部分以Linux为目标但部分代码是纯C的&#xff0c;可以在任何平台上使用。 源码指引&#xff1a;github源…...

单链表基本操作的实现与解析(补充)

目录 一、引言 二、代码实现 遍历考虑情况 三、操作解析 查找操作&#xff08;sltfind函数&#xff09; 前插操作&#xff08;sltinsert函数&#xff09; 后插操作&#xff08;sltinsertafter函数&#xff09; 前删操作&#xff08;slterase函数&#xff09; 后删操作&…...

电子学会—2024年月6青少年软件编程(图形化)四级等级考试真题——魔法门

魔法门 1.准备工作 (1)保留默认角色小猫和白色背景; (2)添加角色Home Button&#xff0c;复制9个造型&#xff0c;在每个造型上分别加上数字1到9&#xff0c;如下图所示; 2.功能实现 (1)程序开始&#xff0c;依次克隆出五个Home Button&#xff0c;克隆体之间的间距为90; …...

《加快应急机器人发展的指导意见》中智能化升级的思考——传统应急设备智能化升级路径与落地实践

感谢阅读本次内容分享&#xff0c;下面我将解读分析《加快应急机器人发展的指导意见》&#xff0c;喜欢的点赞支持一下呗~(日更真的很辛苦~)&#xff0c;欢迎评论区留言讨论&#xff0c;你们的发言我都会看到~ 《加快应急机器人发展的指导意见》中智能化升级的思考——传统应急…...

Git系列之git tag和ReleaseMilestone

以下是关于 Git Tag、Release 和 Milestone 的深度融合内容&#xff0c;并补充了关于 Git Tag 的所有命令、详细解释和指令实例&#xff0c;条理清晰&#xff0c;结合实际使用场景和案例。 1. Git Tag 1.1 定义 • Tag 是 Git 中用于标记特定提交&#xff08;commit&#xf…...

【每日学点HarmonyOS Next知识】Web上传文件、监听上下左右区域连续点击、折叠悬停、字符串相关、播放沙盒视频

1、HarmonyOS APP内h5原生webview input[typefile]无法唤醒手机上传&#xff1f; 文件上传要使用对应的picker https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V5/web-file-upload-V5 Web组件支持前端页面选择文件上传功能&#xff0c;应用开发者可以使用on…...

解决电脑问题(3)——显示器问题

当电脑显示器出现问题时&#xff0c;可以根据不同的故障现象采取相应的解决方法&#xff0c;以下是一些常见的情况及解决措施&#xff1a; 屏幕无显示 检查连接&#xff1a;首先检查显示器与电脑主机之间的视频连接线是否插好&#xff0c;确保两端的接口都牢固连接&#xff0c…...

AArch64架构及其编译器

—1.关于AArch64架构 AArch64是ARMv8-A架构的64位执行状态&#xff0c;支持高性能计算和大内存地址空间。它广泛应用于现代处理器&#xff0c;如苹果的A系列芯片、高通的Snapdragon系列&#xff0c;以及服务器和嵌入式设备。 • 编译器&#xff1a;可以使用GCC、Clang等编译器编…...

免费送源码:Java+springboot+MySQL 房屋租赁系统小程序的设计与实现 计算机毕业设计原创定制

目 录 摘要 1 1 绪论 1 1.1选题意义 1 1.2开发现状 1 1.3springboot框架介绍 1 1.4论文结构与章节安排 1 2 房屋租赁系统小程序系统分析 3 2.1 可行性分析 3 2.1.1 技术可行性分析 3 2.1.2 经济可行性分析 3 2.1.3 法律可行性分析 3 2.2 系统功能分析 3 2.2.1 功…...

前端数据模拟 Mock.js 学习笔记

mock.js介绍 Mock.js是一款前端开发中拦截Ajax请求再生成随机数据响应的工具&#xff0c;可以用来模拟服务器响应 优点是&#xff1a;非常方便简单&#xff0c;无侵入性&#xff0c;基本覆盖常用的接口数据类型支持生成随机的文本、数字、布尔值、日期、邮箱、链接、图片、颜…...

【Linux内核系列】:深入解析输出以及输入重定向

&#x1f525; 本文专栏&#xff1a;Linux &#x1f338;作者主页&#xff1a;努力努力再努力wz ★★★ 本文前置知识&#xff1a; 文件系统以及文件系统调用接口 用c语言简单实现一个shell外壳程序 内容回顾 那么在此前的学习中&#xff0c;我们对于Linux的文件系统已经有了…...

Adam 优化器与动量法:二阶矩与 ODE 的联系

Adam 优化器与动量法&#xff1a;二阶矩与 ODE 的联系 作为深度学习研究者&#xff0c;你一定对 Adam&#xff08;Adaptive Moment Estimation&#xff09;优化器非常熟悉。它因自适应学习率和高效率而成为训练神经网络的标配算法。Adam 使用了一阶动量&#xff08;梯度的指数…...

嵌入式学习第二十三天--网络及TCP

进程通信的方式: 同一主机 传统 system V 不同主机 网络 --- 解决不同主机间 的进程间通信 网络 (通信) //1.物理层面 --- 联通(通路) //卫星 2G 3G 4G 5G 星链 (千帆) //2.逻辑层面 --- 通路(软件) MAC os LINUX …...