您好,登錄后才能下訂單哦!
本篇內容介紹了“高級并發編程系列之什么是CopyOnWriteArrayList”的有關知識,在實際案例的操作過程中,不少人都會遇到這樣的困境,接下來就讓小編帶領大家學習一下如何處理這些情況吧!希望大家仔細閱讀,能夠學有所成!
在你看具體內容前,讓我們一起先思考這么幾個問題:
CopyOnWriteArrayList類名稱中,有我們熟悉的ArrayList,那么日常開發使用ArrayList的時候,有什么你需要注意的地方嗎?
CopyOnWrite中文翻譯過來,是寫時復制,到底什么是寫時復制呢?
關于寫時復制的思想,在什么場景下適合應用,有什么需要注意的地方嗎?
帶著以上幾個問題,讓我們一起開始今天的內容吧。
CopyOnWriteArrayList類名稱中,包含有ArrayList,這表明它們之間具有血緣關系,起源于一個老祖宗,我們先來看類圖:
通過類圖我們看到CopyOnWriteArrayList、ArrayList都實現了相同的接口。為了方便你更好的理解CopyOnWriteArrayList,我們先從ArrayList講起。
接下來我將通過日常開發中使用ArrayList,我將給你分享需要有意識避開的一些案例。
我們知道ArrayList底層是基于數組數據結構實現,它的特性是:擁有數組的一切特性,且支持動態擴容。那么我們使用ArrayList,其實是把它作為容器來使用,對于容器,你能想到都有哪些常規操作嗎?
將元素放入容器中
更新容器中的某個元素
刪除容器中的某個元素
獲取容器中的某個元素
循環遍歷容器中的元素
以上都是我們在項目中,使用容器時的一些高頻操作。對于每個操作,我就不帶著你一一演示了,你應該都很熟悉。這里我們重點關注循環遍歷容器中的元素這個操作。
我們知道容器的循環遍歷操作,可以通過for循環遍歷,還可以通過迭代器循環遍歷。通過上面的類圖,我們知道ArrayList頂層實現了Iterable接口,所以它是支持迭代器操作的,這里迭代器,即應用了迭代器設計模式。關于設計模式的內容,我們暫且不去深究,時間允許的話,我將在下一個系列與你分享我理解的面向對象編程、設計原則、設計思想與設計模式。
接下來我通過ArrayList迭代器遍歷過程中,需要留意的一些地方。我們直接上代碼(show me the code):
package com.anan.edu.common.newthread.collection; import java.util.ArrayList; import java.util.Iterator; /** * 演示ArrayList迭代器遍歷時,需要注意的細節 * * @author ThinkPad * @version 1.0 * @date 2020/12/26 10:50 */ public class ShowMeArrayList { public static void main(String[] args) { // 創建一個ArrayList ArrayList<String> list = new ArrayList<>(); // 添加元素 list.add("zhangsan"); list.add("lisi"); list.add("wangwu"); /* * 正常循環迭代輸出 * */ Iterator<String> iter = list.iterator(); while(iter.hasNext()){ System.out.println("當前從容器中獲取的人是:"+ iter.next()); } } }
執行結果:
當前從容器中獲取的人是:zhangsan 當前從容器中獲取的人是:lisi 當前從容器中獲取的人是:wangwu
通過創建ArrayList實例,添加三個元素:zhangsan 、lisi、wangwu,并通過迭代器進行遍歷輸出。這樣一來我們就準備好了案例基礎案例代碼。
接下來我們做一些演化操作:
在遍歷的過程中,通過ArrayList添加、或者刪除集合中的元素
在遍歷的過程中,通過迭代器Iterator刪除集合中的元素
show me code:
/* * 遍歷過程中,通過Iterator實例:刪除元素 * 預期結果:正常執行 * */ Iterator<String> iter = list.iterator(); while(iter.hasNext()){ // 如果當前遍歷到lisi,我們將lisi從集合中刪除 String name = iter.next(); if("lisi".equals(name)){ iter.remove();// 不會拋出異常 why? } System.out.println("當前從容器中獲取的人是:"+ name); } System.out.println("刪除元素后,集合中還有元素:" + list); // 執行結果 當前從容器中獲取的人是:zhangsan 當前從容器中獲取的人是:lisi 當前從容器中獲取的人是:wangwu 刪除元素后,集合中還有元素:[zhangsan, wangwu] /******************************************************/ /* * 遍歷過程中,通過ArrayList實例:添加、或者刪除元素 * 預期結果:遍歷拋出異常 * */ Iterator<String> iter = list.iterator(); while(iter.hasNext()){ // 如果當前遍歷到lisi,我們向集合中添加:小明 String name = iter.next(); if("lisi".equals(name)){ list.add("小明");// 這行代碼后,繼續迭代器拋出異常 why? } System.out.println("當前從容器中獲取的人是:"+ name); } // 執行結果 當前從容器中獲取的人是:zhangsan Exception in thread "main" java.util.ConcurrentModificationException 當前從容器中獲取的人是:lisi at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:909) at java.util.ArrayList$Itr.next(ArrayList.java:859) at com.anan.edu.common.newthread.collection.ShowMeArrayList.main(ShowMeArrayList.java:31)
上面我們通過案例演示了ArrayList在迭代操作的時候,通過迭代器刪除元素操作,程序不會拋出異常;通過ArrayList添加、刪除,都會引起后續的迭代操作拋出異常。你知道這背后的邏輯嗎?
關于這個問題,我從兩個角度給你分享:
為什么迭代器操作中,不允許向原集合中添加、刪除元素?
ArrayList中,是如何控制迭代操作中,如何檢測原集合是否被添加、刪除操作過?
為了講清楚這個問題,我們從圖開始(一圖勝千言):
高清楚為什么迭代器操作中,不允許向原集合中添加、刪除元素?這個問題后,我們再進一步看ArrayList是如何檢測控制,在迭代過程中,原集合有添加、或者刪除操作這個問題。
這里我將帶你看一下源代碼,這也是我建議你應該要經常做的事情,養成看源代碼習慣,我們常說:源碼之下無秘密。
/* *ArrayList的迭代器,是一個內部類 */ /** * An optimized version of AbstractList.Itr */ private class Itr implements Iterator<E> { // 迭代器內部游標,標識下一個待遍歷元素的數組下標 int cursor; // index of next element to return // 標識已經迭代的最后一個元素的數組下標 int lastRet = -1; // index of last element returned; -1 if no such // 注意:這個變量很重要,它是整個迭代器迭代過程中 // 標識原集合被添加、刪除操作的次數 // 初始值是集合中的成員變量:modCount(集合被添加、刪除操作計數值) int expectedModCount = modCount; Itr() {} ........................ } /* *迭代器 hasNext方法 */ public boolean hasNext() { // 簡單判斷 cursor是否等于 size // 相等,則遍歷結束 // 不相等,則繼續遍歷 return cursor != size; } /* *迭代器 next方法 */ public E next() { // 關鍵代碼:檢查原集合是否被添加、或者刪除操作 // 如果有添加,或者刪除操作,那么expectedModCount != modCount // 拋出異常 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]; } /* *迭代器 checkForComodification方法 */ final void checkForComodification() { if (modCount != expectedModCount) throw new ConcurrentModificationException(); }
通過ArrayList內部類迭代器Itr的源碼分析,我們看到迭代器的源碼實現非常簡答,并且恭喜你!在不知覺中你還學會了迭代器設計模式的實現。
最后我們再通過查看ArrayList中add、remove方法的源碼,解惑modCount成員變量的問題:
/* *ArrayList 的add方法 */ /** * Appends the specified element to the end of this list. * @param e element to be appended to this list * @return <tt>true</tt> (as specified by {@link Collection#add}) */ public boolean add(E e) { // 注釋說了:會將modCount成員變量加1 //繼續看ensureCapacityInternal方法 ensureCapacityInternal(size + 1); // Increments modCount!! elementData[size++] = e; return true; } /* *ArrayList 的ensureCapacityInternal方法 *重點是ensureExplicitCapacity方法 */ private void ensureExplicitCapacity(int minCapacity) { // 將modCount變量加1 modCount++; // overflow-conscious code if (minCapacity - elementData.length > 0) // 擴容操作,留給你去看了 grow(minCapacity); } /* *ArrayList 的remove方法 */ /** * Removes the element at the specified position in this list. * Shifts any subsequent elements to the left (subtracts one from their * indices). * * @param index the index of the element to be removed * @return the element that was removed from the list * @throws IndexOutOfBoundsException {@inheritDoc} */ public E remove(int index) { rangeCheck(index); // 將modCount變量加1 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; }
通過圖、和源碼分析的方式,現在你應該可以更好的理解ArrayList、和它的內部迭代器Itr,并且在你的項目中可以很好的使用ArrayList。
這也是我重點想要分享給你的地方:持續學習,做到知其然,且知其所以然,一種專研的精神。年輕人少刷點抖音、快手、少看點直播,這些東西除了消耗掉你的精氣神外,不會給你帶來任何正向價值的東西。
為了方便你理解CopyOnWriteArrayList,我煞費苦心的帶你一路分析ArrayList。現在讓我們先直觀的看一下CopyOnWriteArrayList。還是通過前面的案例,即迭代器迭代過程中,給原集合添加,或者刪除元素。
我們通過ArrayList演示案例的時候,你還記得吧,會拋出異常,至于異常的原因在前面的內容中,我帶你一起做了專門的分析。如果你不記得了,建議回頭再去看一看。
現在我重點通過CopyOnWriteArrayList來演示案例,看在相同的場景下,是否還會拋出異常?你需要重點關心一下這個地方。
show me the code:
package com.anan.edu.common.newthread.collection; import java.util.Iterator; import java.util.concurrent.CopyOnWriteArrayList; /** * 演示CopyOnWriteArrayList迭代器遍歷時,需要注意的細節 * * @author ThinkPad * @version 1.0 * @date 2020/12/26 10:50 */ public class ShowMeCopyOnWriteArrayList { public static void main(String[] args) { // 創建一個CopyOnWriteArrayList CopyOnWriteArrayList<String> list =new CopyOnWriteArrayList<>(); // 添加元素 list.add("zhangsan"); list.add("lisi"); list.add("wangwu"); /* * 遍歷過程中,通過CopyOnWriteArrayList實例:添加、或者刪除元素 * 預期結果:正常執行 * */ Iterator<String> iter = list.iterator(); while(iter.hasNext()){ // 如果當前遍歷到lisi,我們向集合中添加:小明 String name = iter.next(); if("lisi".equals(name)){ list.add("小明");// 不會拋出異常 why? } System.out.println("當前從容器中獲取的人是:"+ name); } System.out.println("添加元素后,集合中還有元素:" + list); } }
執行結果:
當前從容器中獲取的人是:zhangsan 當前從容器中獲取的人是:lisi 當前從容器中獲取的人是:wangwu 添加元素后,集合中還有元素:[zhangsan, lisi, wangwu, 小明]
通過執行結果看到,使用CopyOnWriteArrayList,在迭代器迭代過程中,向原集合中添加了一個新的元素:小明。迭代器繼續迭代并不會拋出異常,且最后打印結果顯示小明確認已經添加到了集合中。
對于這個結果,你是不是感到多少有點意外!感覺與ArrayList不是一個套路對吧。它到底是如何實現的呢?
剛才我們通過CopyOnWriteArrayList,與ArrayList做了案例演示的對比,發現它們在執行結果上有很大的不一樣。結果差異的本質原因是CopyOnWriteArrayList類名稱中的關鍵字:CopyOnWrite,中文翻譯過來是:寫時復制。
到底什么是寫時復制呢?所謂寫時復制,它直觀的含義是:
我已經有了一個集合A,當需要往集合A中添加一個元素,或者刪除一個元素的時候
保持A集合不變,從A集合復制一個新的集合B
對應向新集合B中添加、或者刪除元素,操作完畢后,將A指向新的B集合,即用新的集合,替換舊的集合
你看這就是寫時復制的思想,理解起來并不困難。這樣做有什么好處呢?好處就是當我們通過迭代器訪問集合的時候,我們可以同時允許向集合中添加、刪除集合元素,有效避免了訪問集合(讀操作),與更新集合(寫操作)的沖突,最大化實現了集合的并發訪問性能。
那么關于CopyOnWriteArrayList,它是如何最大化提升并發訪問能力呢?它的實現原理并不復雜,既然是并發訪問,線程安全的問題不可回避,你應該也想到了,首先加鎖是必須的。
除了加鎖,還需要考慮提升并發訪問的能力,如何提升?實現也很簡單,針對寫操作加鎖,讀操作不加鎖。這樣一來,即最大化提升了并發訪問的能力,非常適合應用在讀多寫少的業務場景。這其實也是我們在項目中,使用CopyOnWriteArrayList的一個主要應用場景。
通過前面兩個小結,我們已經搞清楚CopyOnWriteArrayList的應用場景,并理解了什么是寫時復制的思想。在你的項目中,根據業務需要,我們在進行業務結構設計的時候,可以借鑒寫時復制的這一思想,解決實際的業務問題。一定要學會活學活用,至于如何發揮,就留給你了。
接下來我帶你一起看一下CopyOnWriteArrayList關鍵方法的源碼實現,進一步加深你對寫時復制思想的理解,我們通過兩個主要的集合操作來看,分別是:
添加集合元素(寫操作):add
/** * Appends the specified element to the end of this list. * * @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(); } }
訪問集合元素(讀操作):get
/** * {@inheritDoc} * * @throws IndexOutOfBoundsException {@inheritDoc} */ public E get(int index) { // 獲取集合中的元素,讀操作不需要加鎖 return get(getArray(), index); } private E get(Object[] a, int index) { return (E) a[index]; }
通過add、get方法源碼,驗證了我們前面分析的結論:寫操作加鎖、讀操作不需要加鎖。
最后我們以一個問答的形式結束本次分享,寫時復制思想適合應用在讀多寫少的業務場景下,最大化提升集合的并發訪問能力。我們說:任何事物都有兩面性,你知道它的另一面存在什么局限性嗎?
我們直接給出答案,寫時復制思想的局限性是:
更加消耗空間資源,寫操作要從舊的集合,復制得到一個新的集合,即新舊集合同時存在,更占用內存資源
另外寫操作加鎖,讀操作不加鎖的實現方式,會存在過期讀的問題
結合以上兩點,當你在項目中應用寫時復制思想進行業務架構設計的時候,或者使用CopyOnWriteArrayList的時候,一定要考慮業務上是否能夠接受過期讀的問題。
“高級并發編程系列之什么是CopyOnWriteArrayList”的內容就介紹到這里了,感謝大家的閱讀。如果想了解更多行業相關的知識可以關注億速云網站,小編將為大家輸出更多高質量的實用文章!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。