您好,登錄后才能下訂單哦!
這篇文章主要介紹“JUC的CopyOnWriteArrayList怎么理解”,在日常操作中,相信很多人在JUC的CopyOnWriteArrayList怎么理解問題上存在疑惑,小編查閱了各式資料,整理出簡單好用的操作方法,希望對大家解答”JUC的CopyOnWriteArrayList怎么理解”的疑惑有所幫助!接下來,請跟著小編一起來學習吧!
CopyOnWriteArrayList 是線程安全的 List 實現,底層依賴于數組作為存儲結構,并基于 寫時復制(CoW: Copy-on-Write)機制 保證線程安全性。CopyOnWriteArrayList 在執行修改操作時會將底層數組復制一份,并在副本上實施修改,最后再更新回底層數組。雖然這樣的實現比較消耗內存,但卻帶來了較高的執行效率,屬于拿空間換時間。
除了 CopyOnWriteArrayList,在 JUC 包中還有另外一個基于 CoW 機制實現的線程安全組件,即 CopyOnWriteArraySet,不過 CopyOnWriteArraySet 本質上還是基于 CopyOnWriteArrayList 實現的,所以理解了 CopyOnWriteArrayList 的實現原理,也就同時理解了 CopyOnWriteArraySet 的實現原理。
CopyOnWriteArrayList 實現自 List 接口,所以我們可以像使用 ArrayList 一樣使用 CopyOnWriteArrayList。CopyOnWriteArrayList 類的字段定義如下:
public class CopyOnWriteArrayList<E> implements List<E>, RandomAccess, Cloneable, Serializable { /** 支撐同步操作的重入鎖 */ final transient ReentrantLock lock = new ReentrantLock(); /** 底層數組 */ private transient volatile Object[] array; // ... 省略方法定義 }
其中 CopyOnWriteArrayList#array
數組是 CopyOnWriteArrayList 的底層存儲結構,CopyOnWriteArrayList 依賴于該數組對數據進行存儲。字段 CopyOnWriteArrayList#lock
對應 ReentrantLock 類型,用于控制多個線程對底層數組的修改操作,保證同一時間只有一個線程對底層數組進行修改。CopyOnWriteArrayList 提供了相應的 CopyOnWriteArrayList#getArray
和 CopyOnWriteArrayList#setArray
方法用于訪問該字段。
CopyOnWriteArrayList 定義了多個重載的構造方法來初始化構造一個 CopyOnWriteArrayList 對象,實現上比較簡單。下面重點看一下 CopyOnWriteArrayList 之于 List 接口中聲明的核心方法實現,即增(add)、刪(remove)、改(set)、查(get)操作。
首先來看一下 添加元素 的操作,CopyOnWriteArrayList 定義了多個重載版本的 CopyOnWriteArrayList#add
的方法實現,包括:
CopyOnWriteArrayList#add(E)
CopyOnWriteArrayList#add(int, E)
CopyOnWriteArrayList#addIfAbsent(E)
CopyOnWriteArrayList#addAll(Collection<? extends E>)
CopyOnWriteArrayList#addAll(int, Collection<? extends E>)
CopyOnWriteArrayList#addAllAbsent
這些方法在實現上思想都是相通的,下面以 CopyOnWriteArrayList#add(E)
方法為例分析往 CopyOnWriteArrayList 中添加元素的運行機制,方法實現如下:
public boolean add(E e) { final ReentrantLock lock = this.lock; // 加鎖,保證同一時間只有一個線程修改底層數組 lock.lock(); try { // 獲取底層數組 Object[] elements = this.getArray(); int len = elements.length; // 復制出一份新的數組,長度加 1,以容納待添加的元素 Object[] newElements = Arrays.copyOf(elements, len + 1); newElements[len] = e; // 更新底層數組 this.setArray(newElements); return true; } finally { // 釋放鎖 lock.unlock(); } }
前面我們已經提及到 CopyOnWriteArrayList 對于數據的修改都是在副本上進行的,上述方法的實現印證了這一點。當往 CopyOnWriteArrayList 中添加一個元素時,方法的執行流程如下:
獲取鎖對象,保證同一時間只有一個線程在執行修改操作;
獲取底層數組,基于該數組拷貝出一個新的數組,新數組長度加 1;
追加待添加元素到新數組的末尾位置,并更新底層數組;
釋放鎖對象。
再來看一下 獲取元素 的操作,CopyOnWriteArrayList 僅定義了一個 CopyOnWriteArrayList#get
方法,用于獲取指定下標的元素值,實現如下:
public E get(int index) { // 直接返回底層數組 index 下標的元素 return this.get(this.getArray(), index); } private E get(Object[] a, int index) { return (E) a[index]; }
實現上比較簡單,這里主要強調一下弱一致性問題,也就說 get 操作不一定能夠返回最新的值。考慮這樣一個場景,假設有線程 A 和 B,其中 A 調用 get 方法獲取數據,B 調用 add 方法添加數據,當前數組長度為 5:
線程 B 獲取底層數組 x,然后拷貝出一個長度為 6 的新數組 y,并將待追加的元素設置到 y[5]
的位置,此時還未更新底層數組;
因為讀操作無需獲取鎖,如果此時線程 A 嘗試獲取下標為 5 的元素,則會拋出 IndexOutOfBoundsException,因為此時 A 獲取到的底層數組還是 x,還沒有更新成 y。
然而,大部分時候這種弱一致性并不會對我們的業務造成影響,但是我們需要知道其存在,以便在發生錯誤時快速定位問題。
繼續來看一下 修改元素 的操作,CopyOnWriteArrayList 同樣僅定義了一個 CopyOnWriteArrayList#set
方法,用于修改指定下標的元素值,實現如下:
public E set(int index, E element) { final ReentrantLock lock = this.lock; // 獲取鎖 lock.lock(); try { // 獲取底層數組 Object[] elements = this.getArray(); // 獲取數組 index 下標值 E oldValue = this.get(elements, index); // 待更新的元素值與數組中指定位置的元素值不同 if (oldValue != element) { int len = elements.length; Object[] newElements = Arrays.copyOf(elements, len); // 更新 index 位置的元素值 newElements[index] = element; // 更新底層數組 this.setArray(newElements); } else { // Not quite a no-op; ensures volatile write semantics // 待更新的元素值與數組中指定位置的元素值相同,無需執行更新操作 this.setArray(elements); // 保證 volatile 寫語義 } return oldValue; } finally { // 釋放鎖 lock.unlock(); } }
上述方法的執行邏輯如代碼注釋,比較簡單,但是當修改的目標元素值前后未發生變化時還是會調用 CopyOnWriteArrayList#setArray
方法更新一下底層數組,用意何在呢?
代碼中給出的理由是 “Not quite a no-op; ensures volatile write semantics”,也就是說這里的更新操作不完全是一個無意義的操作,可以用來保證 volatile 的寫語義,因為底層數組 array 是 volatile 類型。要理解其用意,可以參考 《深入理解 java 內存模型》一書中的示例(稍作修改):
private int a = 0; private volatile boolean flag = true; public void writer() { a = 1; // 1 flag = true; // 2 } public void reader() { if (flag) { // 3 int i = a; // 4 } }
假設線程 A 執行 writer 方法之后,線程 B 執行 reader 方法。根據 happens-before 原則,這個過程建立的 happens-before 關系為:
根據程序次序規則,1 happens before 2; 3 happens before 4。
根據 volatile 規則,2 happens before 3。
根據 happens before 的傳遞性規則,1 happens before 4。
這樣就能保證線程 B 在 reader 方法中讀取 a 變量時能夠看見線程 A 在 writer 方法中對 a 的修改,即使在 writer 方法中對變量 flag 的修改同樣看似多余。
最后來看一下 刪除元素 的操作,CopyOnWriteArrayList 針對刪除操作定義了多個重載版本的 CopyOnWriteArrayList#remove
的方法實現,包括:
CopyOnWriteArrayList#remove(int)
CopyOnWriteArrayList#remove(java.lang.Object)
CopyOnWriteArrayList#removeAll
CopyOnWriteArrayList#removeIf
下面以 CopyOnWriteArrayList#remove(int)
方法為例分析從 CopyOnWriteArrayList 中刪除元素的運行機制,方法實現如下:
public E remove(int index) { final ReentrantLock lock = this.lock; // 獲取鎖 lock.lock(); try { // 獲取底層數組 Object[] elements = this.getArray(); int len = elements.length; // 獲取指定下標元素 E oldValue = this.get(elements, index); int numMoved = len - index - 1; // 如果是刪除最后一個元素,則直接 copy 即可,無需移動 if (numMoved == 0) { this.setArray(Arrays.copyOf(elements, len - 1)); } else { // 否則,分兩次進行拷貝,去掉原 index 下標的元素 Object[] newElements = new Object[len - 1]; System.arraycopy(elements, 0, newElements, 0, index); System.arraycopy(elements, index + 1, newElements, index, numMoved); this.setArray(newElements); } return oldValue; } finally { // 釋放鎖 lock.unlock(); } }
刪除的邏輯本質上還是在副本上進行,如上述代碼注釋,其過程與前面分析的操作類似。
除了上面分析的核心操作方法,CopyOnWriteArrayList 還實現了 CopyOnWriteArrayList#iterator
方法返回一個迭代器,實現如下:
public Iterator<E> iterator() { return new COWIterator<E>(this.getArray(), 0); }
關于 COWIterator 的實現不再繼續深入,但是需要知曉的一點是,COWIterator 不支持在迭代過程中修改 CopyOnWriteArrayList 中的元素,對應的 COWIterator#remove
、COWIterator#set
和 COWIterator#add
方法均直接拋出 UnsupportedOperationException 異常。此外,方法 CopyOnWriteArrayList#iterator
返回的是一個弱一致性迭代器,即在迭代期間,其它線程對于底層數組的修改并不會被迭代器看見。
到此,關于“JUC的CopyOnWriteArrayList怎么理解”的學習就結束了,希望能夠解決大家的疑惑。理論與實踐的搭配能更好的幫助大家學習,快去試試吧!若想繼續學習更多相關知識,請繼續關注億速云網站,小編會繼續努力為大家帶來更多實用的文章!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。