你們在學習Java的過程當中,或者工做中,始終都繞不開集合。在單線程環境下,ArrayList就能夠知足要求。多線程時,咱們能夠使用CopyOnWriteArrayList來保證數據安全。下面咱們一塊兒來看看CopyOnWriteArrayList類中的一些值得學習的方法。java
說明:代碼部分,均基於JDK1.8數組
CopyOnWrite, 簡稱COW,顧名思義,就是寫入的時候將當前集合複製一份副本出來,新寫入的值添加到副本集合裏,再將原集合的引用指向新的副本集合。基於這個原理,就能夠不加鎖實現併發讀,由於當前集合並不會添加元素,不會形成衝突。一樣的原理還應用在MySQL中建立快照的過程。安全
/** * 將指定的元素追加到此列表的末尾 * * @param e element to be appended to this list * @return {@code true} (as specified by {@link Collection#add}) */ public boolean add(E e) { // 聲明可重入鎖 final ReentrantLock lock = this.lock; // 加鎖 lock.lock(); try { // 獲取當前數組 Object[] elements = getArray(); int len = elements.length; // 複製當前數組到一個新數組中 Object[] newElements = Arrays.copyOf(elements, len + 1); // 添加元素 newElements[len] = e; // 改變引用 setArray(newElements); return true; } finally { lock.unlock(); } }
你們在學習Java期間,必定都有過使用forEach遍歷ArrayList時刪除元素都會獲得一個java.util.ConcurrentModificationException的錯誤。這是由於在ArrayList的remove()方法中,有一個參數modCount 專門用來記錄修改的次數,每刪除一次就modCount++。在forEach遍歷集合時,首先會記錄final int expectedModCount = modCount,如果遍歷過程當中發現expectedModCount!=modCount,則會拋出錯誤。
多線程
下面來看看具體代碼併發
/** * 刪除元素 */ public E remove(int index) { // 檢查下標是否越界 rangeCheck(index); // 記錄修改次數 modCount++; // 待刪除的元素 E oldValue = elementData(index); // 待刪除元素下標以後的數組長度 int numMoved = size - index - 1; if (numMoved > 0) // 刪除元素 System.arraycopy(elementData, index+1, elementData, index, numMoved); elementData[--size] = null; // clear to let GC do its work return oldValue; } /** * forEach 方法 */ @Override public void forEach(Consumer<? super E> action) { Objects.requireNonNull(action); // 記錄modCount final int expectedModCount = modCount; @SuppressWarnings("unchecked") final E[] elementData = (E[]) this.elementData; final int size = this.size; // 遍歷時判斷modCount for (int i=0; modCount == expectedModCount && i < size; i++) { action.accept(elementData[i]); } // 不相同,拋出異常 if (modCount != expectedModCount) { throw new ConcurrentModificationException(); } }
因此使用ArrayList時,若是你有遍歷刪除某個元素的場景,咱們能夠使用迭代器來刪除。
app
先來看看CopyOnWriteArrayList的remove()方法的源碼,總體邏輯與ArrayList的remove()方法一直,有區別的是沒有記錄修改次數,由於每次刪除都是從新獲取的當前數組。而forEach()方法在遍歷時也是獲取的當前數組,所以在使用forEach遍歷時刪除元素不會拋出異常。ide
/** * 刪除元素 */ public E remove(int index) { final ReentrantLock lock = this.lock; // 加鎖 lock.lock(); try { // 獲取當前數組 Object[] elements = getArray(); // 記錄數組的長度 int len = elements.length; // 記錄待刪除元素 E oldValue = get(elements, index); // 記錄待刪除元素後一個元素到尾節點的長度 int numMoved = len - index - 1; // 爲0表明待刪除元素就在數組的末尾 if (numMoved == 0) setArray(Arrays.copyOf(elements, len - 1)); else { Object[] newElements = new Object[len - 1]; // 複製到新數組中 System.arraycopy(elements, 0, newElements, 0, index); System.arraycopy(elements, index + 1, newElements, index, numMoved); // 改變引用 setArray(newElements); } return oldValue; } finally { lock.unlock(); } } /** * 遍歷方法 */ public void forEach(Consumer<? super E> action) { if (action == null) throw new NullPointerException(); // 獲取當前數組 Object[] elements = getArray(); int len = elements.length; for (int i = 0; i < len; ++i) { @SuppressWarnings("unchecked") E e = (E) elements[i]; action.accept(e); } }
值得注意的是,CopyOnWriteArrayList的迭代器實現裏的remove()方法會直接拋出異常,所以在使用迭代器遍歷元素時,不能刪除元素。學習
基於原理,不難分析出CopyOnWriteArrayList適用於讀多寫少的併發環境ui
由於每次添加元素都須要複製一份副本,因此最好是使用批量添加,減小複製副本的次數this
1、內存佔用問題。 由於 CopyOnWrite 的寫時複製機制,因此在進行寫操做的時候,內存裏會同時駐紮兩個對象的內存,這一點會佔用額外的內存空間。
2、數據一致性問題。 因爲 CopyOnWrite 容器的修改是先修改副本,因此此次修改對於其餘線程來講,並非實時能看到的,只有在修改完以後才能體現出來。若是你但願寫入的的數據立刻能被其餘線程看到,CopyOnWrite 容器是不適用的。