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 的適用場景、讀寫規則,還介紹了它的兩個特點,分別是寫時復制和迭代期間允許修改集合內容。我們還介紹了它的三個缺點,分別是內存占用問題,在元素較多或者復雜的情況下復制的開銷大問題,以及數據一致性問題。最后我們對于它的重要源碼進行了解析。

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/bicheng/72840.shtml
繁體地址,請注明出處:http://hk.pswp.cn/bicheng/72840.shtml
英文地址,請注明出處:http://en.pswp.cn/bicheng/72840.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

Jmeter接口測試詳解

今天筆者呢&#xff0c;想給大家聊聊Jmeter接口測試流程詳解&#xff0c;廢話不多說直接進入正題。 一、jmeter簡介 Jmeter是由Apache公司開發的java開源項目&#xff0c;所以想要使用它必須基于java環境才可以&#xff1b; Jmeter采用多線程&#xff0c;允許通過多個線程并…

DeepSeek開啟AI辦公新模式,WPS/Office集成DeepSeek-R1本地大模型!

從央視到地方媒體&#xff0c;已有多家媒體機構推出AI主播&#xff0c;最近杭州文化廣播電視集團的《杭州新聞聯播》節目&#xff0c;使用AI主持人進行新聞播報&#xff0c;且做到了0失誤率&#xff0c;可見AI正在逐漸取代部分行業和一些重復性的工作&#xff0c;這一現象引發很…

通過Golang的container/list實現LRU緩存算法

文章目錄 力扣&#xff1a;146. LRU 緩存主要結構 List 和 Element常用方法1. 初始化鏈表2. 插入元素3. 刪除元素4. 遍歷鏈表5. 獲取鏈表長度使用場景注意事項 源代碼閱讀 在 Go 語言中&#xff0c;container/list 包提供了一個雙向鏈表的實現。鏈表是一種常見的數據結構&#…

【大學生體質】智能 AI 旅游推薦平臺(Vue+SpringBoot3)-完整部署教程

智能 AI 旅游推薦平臺開源文檔 項目前端地址 ??項目介紹 智能 AI 旅游推薦平臺&#xff08;Intelligent AI Travel Recommendation Platform&#xff09;是一個利用 AI 模型和數據分析為用戶提供個性化旅游路線推薦、景點評分、旅游攻略分享等功能的綜合性系統。該系統融合…

【滲透測試】基于時間的盲注(Time-Based Blind SQL Injection)

發生ERROR日志告警 查看系統日志如下&#xff1a; java.lang.IllegalArgumentException: Illegal character in query at index 203: https://api.weixin.qq.com/sns/jscode2session?access_token90_Vap5zo5UTJS4jbuvneMkyS1LHwHAgrofaX8bnIfW8EHXA71IRZwsqzJam9bo1m3zRcSrb…

redis數據類型以及底層數據結構

redis數據類型以及底層數據結構 String&#xff1a;字符串類型&#xff0c;底層就是動態字符串&#xff0c;使用sds數據結構 Map:有兩種數據結構&#xff1a;1.壓縮列表&#xff1a;當hash結構中存儲的元素個數小于了512個。并且元 …

DeepSeek R1-32B醫療大模型的完整微調實戰分析(全碼版)

DeepSeek R1-32B微調實戰指南 ├── 1. 環境準備 │ ├── 1.1 硬件配置 │ │ ├─ 全參數微調:4*A100 80GB │ │ └─ LoRA微調:單卡24GB │ ├── 1.2 軟件依賴 │ │ ├─ PyTorch 2.1.2+CUDA │ │ └─ Unsloth/ColossalAI │ └── 1.3 模…

window下的docker內使用gpu

Windows 上使用 Docker GPU需要進行一系列的配置和步驟。這是因為 Docker 在 Windows 上的運行環境與 Linux 有所不同,需要借助 WSL 2(Windows Subsystem for Linux 2)和 NVIDIA Container Toolkit 來實現 GPU 的支持。以下是詳細的流程: 一、環境準備 1.系統要求 Window…

Ubuntu 下 nginx-1.24.0 源碼分析 - cycle->modules[i]->type

Nginx 中主要有以下幾種模塊類型 類型 含義 NGX_CORE_MODULE 核心模塊&#xff08;如進程管理、錯誤日志、配置解析&#xff09;。 NGX_EVENT_MODULE 事件模塊&#xff08;如 epoll、kqueue 等 IO 多路復用機制的實現&#xff09;。 NGX_HTTP_MODULE HTTP 模塊&#xf…

八、排序算法

一些簡單的排序算法 8.1 冒泡排序 void Bubble_sort(int a[] , int len){int i,j,flag,tmp;for(i=0 ; i < len-1 ; i++){flag = 1;for(j=0 ; j < len-1-i ; j++){if(a[j] > a[j+1]){tmp = a[j];a[j] = a[j+1];a[j+1] = tmp;flag = 0;}}if(flag == 1){break;}}…

Sqlserver安全篇之_手工創建TLS用到的pfx證書文件

Sqlserver官方提供的Windows Powershell腳本 https://learn.microsoft.com/zh-cn/sql/database-engine/configure-windows/configure-sql-server-encryption?viewsql-server-ver16 # Define parameters $certificateParams {Type "SSLServerAuthentication"Subje…

npm install -g @vue/cli 方式已經無法創建VUE3項目

采用該方式&#xff0c;啟動VUE3項目&#xff0c;運行命令&#xff0c;出現報錯&#xff1a; npm install -g vue/cli PS D:\> npm install -g vue/cli npm warn deprecated inflight1.0.6: This module is not supported, and leaks memory. Do not use it. Check out lr…

3.8[a]cv

函數核心目標 實現屏幕空間內三角形的光柵化&#xff0c;將三角形覆蓋的像素點顏色填充到幀緩沖區&#xff0c;同時處理深度測試&#xff08;Z-Buffer&#xff09;。這是渲染管線中幾何階段到像素階段的關鍵步驟 包圍盒計算&#xff08;Bounding Box&#xff09;?** ?功能&…

導入 Excel 規則批量修改或刪除 Excel 表格內容

我們前面介紹過按照規則批量修改 Excel 文檔內容的操作&#xff0c;可以對大量的 Excel 文檔按照一定的規則進行統一的修改&#xff0c;可以很好的解決我們批量修改 Excel 文檔內容的需求。但是某些場景下&#xff0c;我們批量修改 Excel 文檔內容的場景比較復雜&#xff0c;比…

SGLang Router:基于緩存感知負載均衡的數據并行路由實踐

SGLang Router&#xff1a;基于緩存感知負載均衡的數據并行路由實踐 一、引言二、安裝與快速啟動三、兩種工作模式對比3.1 協同啟動模式&#xff08;單節點&#xff09;3.2 獨立啟動模式&#xff08;多節點&#xff09; 四、動態擴縮容API4.1 添加Worker節點4.2 移除Worker節點…

在人工智能軟件的幫助下學習編程實例

1 引言 本文記錄在人工智能軟件的幫助下學習一種全新的編程環境的實例&#xff0c;之所以提人工智能軟件而不是單指DeepSeek&#xff0c;一方面DeepSeek太火了&#xff0c;經常服務器繁忙&#xff0c;用本機本地部署的最多運行70b模型&#xff0c;又似乎稍差。另一方面也作為一…

Ubuntu 下 nginx-1.24.0 源碼分析 - ngx_modules

定義在 objs\ngx_modules.c #include <ngx_config.h> #include <ngx_core.h>extern ngx_module_t ngx_core_module; extern ngx_module_t ngx_errlog_module; extern ngx_module_t ngx_conf_module; extern ngx_module_t ngx_openssl_module; extern ngx_modul…

深度學習代碼解讀——自用

代碼來自&#xff1a;GitHub - ChuHan89/WSSS-Tissue 借助了一些人工智能 2_generate_PM.py 功能總結 該代碼用于 生成弱監督語義分割&#xff08;WSSS&#xff09;所需的偽掩碼&#xff08;Pseudo-Masks&#xff09;&#xff0c;是 Stage2 訓練的前置步驟。其核心流程為&a…

Java基礎面試題全集

1. Java語言基礎 1.1 Java是什么&#xff1f; ? Java是一種廣泛使用的編程語言&#xff0c;最初由Sun Microsystems&#xff08;現為Oracle公司的一部分&#xff09;于1995年發布。它是一種面向對象的、基于類的、通用型的編程語言&#xff0c;旨在讓應用程序“編寫一次&…

Selenium遇到Exception自動截圖

# 隨手小記 場景&#xff1a;測試百度&#xff1a; 點擊新聞&#xff0c;跳轉到新的窗口&#xff0c;找到輸入框&#xff0c;輸入“hello,world" 等到輸入框的內容是hello,world, 這里有個錯誤&#xff0c;少了一個] 后來就實現了錯誤截圖的功能&#xff0c;可以參考 …