摘要:今天主要講解的是本文力求簡單講清每個知識點,希望大家看完能有所收獲一和回顧線程安全的和我們知道是用于替代的,是線程安全的容器。使用迭代器遍歷時不需要顯示加鎖,看看與方法的實現可能就有點眉目了。
前言
只有光頭才能變強
前一陣子寫過一篇COW(Copy On Write)文章,結果閱讀量很低啊...COW奶牛!Copy On Write機制了解一下
可能大家對這個技術比較陌生吧,但這項技術是挺多應用場景的。除了上文所說的Linux、文件系統外,其實在Java也有其身影。
大家對線程安全容器可能最熟悉的就是ConcurrentHashMap了,因為這個容器經常會在面試的時候考查。
比如說,一個常見的面試場景:
面試官問:“HashMap是線程安全的嗎?如果HashMap線程不安全的話,那有沒有安全的Map容器”
3y:“線程安全的Map有兩個,一個是Hashtable,一個是ConcurrentHashMap”
面試官繼續問:“那Hashtable和ConcurrentHashMap有什么區別啊?”
3y:“balabalabalabalabalabala"
面試官:”ok,ok,ok,看你Java基礎挺不錯的呀“
那如果有這樣的面試呢?
面試官問:“ArrayList是線程安全的嗎?如果ArrayList線程不安全的話,那有沒有安全的類似ArrayList的容器”
3y:“線程安全的ArrayList我們可以使用Vector,或者說我們可以使用Collections下的方法來包裝一下”
面試官繼續問:“嗯,我相信你也知道Vector是一個比較老的容器了,還有沒有其他的呢?”
3y:“Emmmm,這個...“
面試官提示:“就比如JUC中有ConcurrentHashMap,那JUC中有類似"ArrayList"的線程安全容器類嗎?“
3y:“Emmmm,這個...“
面試官:”ok,ok,ok,今天的面試時間也差不多了,你回去等通知吧。“
今天主要講解的是CopyOnWriteArrayList~
本文力求簡單講清每個知識點,希望大家看完能有所收獲
一、Vector和SynchronizedList 1.1回顧線程安全的Vector和SynchronizedList我們知道ArrayList是用于替代Vector的,Vector是線程安全的容器。因為它幾乎在每個方法聲明處都加了synchronized關鍵字來使容器安全。
如果使用Collections.synchronizedList(new ArrayList())來使ArrayList變成是線程安全的話,也是幾乎都是每個方法都加上synchronized關鍵字的,只不過它不是加在方法的聲明處,而是方法的內部。
1.2Vector和SynchronizedList可能會出現的問題在講解CopyOnWrite容器之前,我們還是先來看一下線程安全容器的一些可能沒有注意到的地方~
下面我們直接來看一下這段代碼:
// 得到Vector最后一個元素 public static Object getLast(Vector list) { int lastIndex = list.size() - 1; return list.get(lastIndex); } // 刪除Vector最后一個元素 public static void deleteLast(Vector list) { int lastIndex = list.size() - 1; list.remove(lastIndex); }
以我們第一反應來分析一下上面兩個方法:在多線程環境下,是否有問題?
我們可以知道的是Vector的size()和get()以及remove()都被synchronized修飾的。
答案:從調用者的角度是有問題的
我們可以寫段代碼測試一下:
import java.util.Vector; public class UnsafeVectorHelpers { public static void main(String[] args) { // 初始化Vector Vectorvector = new Vector(); vector.add("關注公眾號"); vector.add("Java3y"); vector.add("買Linux可到我下面的鏈接,享受最低價"); vector.add("給3y加雞腿"); new Thread(() -> getLast(vector)).start(); new Thread(() -> deleteLast(vector)).start(); new Thread(() -> getLast(vector)).start(); new Thread(() -> deleteLast(vector)).start(); } // 得到Vector最后一個元素 public static Object getLast(Vector list) { int lastIndex = list.size() - 1; return list.get(lastIndex); } // 刪除Vector最后一個元素 public static void deleteLast(Vector list) { int lastIndex = list.size() - 1; list.remove(lastIndex); } }
可以發現的是,有可能會拋出異常的:
原因也很簡單,我們照著流程走一下就好了:
線程A執行getLast()方法,線程B執行deleteLast()方法
線程A執行int lastIndex = list.size() - 1;得到lastIndex的值是3。同時,線程B執行int lastIndex = list.size() - 1;得到的lastIndex的值也是3
此時線程B先得到CPU執行權,執行list.remove(lastIndex)將下標為3的元素刪除了
接著線程A得到CPU執行權,執行list.get(lastIndex);,發現已經沒有下標為3的元素,拋出異常了.
出現這個問題的原因也很簡單:
getLast()和deleteLast()這兩個方法并不是原子性的,即使他們內部的每一步操作是原子性的(被Synchronize修飾就可以實現原子性),但是內部之間還是可以交替執行。
這里的意思就是:size()和get()以及remove()都是原子性的,但是如果并發執行getLast()和deleteLast(),方法里面的size()和get()以及remove()是可以交替執行的。
要解決上面這種情況也很簡單,因為我們都是對Vector進行操作的,只要操作Vector前把它鎖住就沒毛病了!
所以我們可以改成這樣子:
// 得到Vector最后一個元素 public static Object getLast(Vector list) { synchronized (list) { int lastIndex = list.size() - 1; return list.get(lastIndex); } } // 刪除Vector最后一個元素 public static void deleteLast(Vector list) { synchronized (list) { int lastIndex = list.size() - 1; list.remove(lastIndex); } }
ps:如果有人去測試一下,發現會拋出異常java.lang.ArrayIndexOutOfBoundsException: -1,這是沒有檢查角標的異常,不是并發導致的問題。
經過上面的例子我們可以看看下面的代碼:
public static void main(String[] args) { // 初始化Vector Vectorvector = new Vector(); vector.add("關注公眾號"); vector.add("Java3y"); vector.add("買Linux可到我下面的鏈接,享受最低價"); vector.add("給3y加雞腿"); // 遍歷Vector for (int i = 0; i < vector.size(); i++) { // 比如在這執行vector.clear(); //new Thread(() -> vector.clear()).start(); System.out.println(vector.get(i)); } }
同樣地:如果在遍歷Vector的時候,有別的線程修改了Vector的長度,那還是會有問題!
線程A遍歷Vector,執行vector.size()時,發現Vector的長度為5
此時很有可能存在線程B對Vector進行clear()操作
隨后線程A執行vector.get(i)時,拋出異常
在JDK5以后,Java推薦使用for-each(迭代器)來遍歷我們的集合,好處就是簡潔、數組索引的邊界值只計算一次。
如果使用for-each(迭代器)來做上面的操作,會拋出ConcurrentModificationException異常
SynchronizedList在使用迭代器遍歷的時候同樣會有問題的,源碼已經提醒我們要手動加鎖了。
如果想要完美解決上面所講的問題,我們可以在遍歷前加鎖:
// 遍歷Vector synchronized (vector) { for (int i = 0; i < vector.size(); i++) { vector.get(i); } }
有經驗的同學就可以知道:哇,遍歷一下容器都要我加上鎖,這這這不是要慢死了嗎.的確是挺慢的..
所以我們的CopyOnWriteArrayList就登場了!
二、CopyOnWriteArrayList(Set)介紹一般來說,我們會認為:CopyOnWriteArrayList是同步List的替代品,CopyOnWriteArraySet是同步Set的替代品。
無論是Hashtable-->ConcurrentHashMap,還是說Vector-->CopyOnWriteArrayList。JUC下支持并發的容器與老一代的線程安全類相比,總結起來就是加鎖粒度的問題
Hashtable、Vector加鎖的粒度大(直接在方法聲明處使用synchronized)
ConcurrentHashMap、CopyOnWriteArrayList加鎖粒度小(用各種的方式來實現線程安全,比如我們知道的ConcurrentHashMap用了cas鎖、volatile等方式來實現線程安全..)
JUC下的線程安全容器在遍歷的時候不會拋出ConcurrentModificationException異常
所以一般來說,我們都會使用JUC包下給我們提供的線程安全容器,而不是使用老一代的線程安全容器。
下面我們來看看CopyOnWriteArrayList是怎么實現的,為什么使用迭代器遍歷的時候就不用額外加鎖,也不會拋出ConcurrentModificationException異常。
2.1CopyOnWriteArrayList實現原理我們還是先來回顧一下COW:
如果有多個調用者(callers)同時請求相同資源(如內存或磁盤上的數據存儲),他們會共同獲取相同的指針指向相同的資源,直到某個調用者試圖修改資源的內容時,系統才會真正復制一份專用副本(private copy)給該調用者,而其他調用者所見到的最初的資源仍然保持不變。優點是如果調用者沒有修改該資源,就不會有副本(private copy)被建立,因此多個調用者只是讀取操作時可以共享同一份資源。
參考自維基百科:https://zh.wikipedia.org/wiki/%E5%AF%AB%E5%85%A5%E6%99%82%E8%A4%87%E8%A3%BD
之前寫博客的時候,如果是要看源碼,一般會翻譯一下源碼的注釋并用圖貼在文章上的。Emmm,發現閱讀體驗并不是很好,所以我這里就直接概括一下源碼注釋說了什么吧。另外,如果使用IDEA的話,可以下一個插件Translation(免費好用).
概括一下CopyOnWriteArrayList源碼注釋介紹了什么:
CopyOnWriteArrayList是線程安全容器(相對于ArrayList),底層通過復制數組的方式來實現。
CopyOnWriteArrayList在遍歷的使用不會拋出ConcurrentModificationException異常,并且遍歷的時候就不用額外加鎖
元素可以為null
2.1.1看一下CopyOnWriteArrayList基本的結構/** 可重入鎖對象 */ final transient ReentrantLock lock = new ReentrantLock(); /** CopyOnWriteArrayList底層由數組實現,volatile修飾 */ private transient volatile Object[] array; /** * 得到數組 */ final Object[] getArray() { return array; } /** * 設置數組 */ final void setArray(Object[] a) { array = a; } /** * 初始化CopyOnWriteArrayList相當于初始化數組 */ public CopyOnWriteArrayList() { setArray(new Object[0]); }
看起來挺簡單的,CopyOnWriteArrayList底層就是數組,加鎖就交由ReentrantLock來完成。
2.1.2常見方法的實現根據上面的分析我們知道如果遍歷Vector/SynchronizedList是需要自己手動加鎖的。
CopyOnWriteArrayList使用迭代器遍歷時不需要顯示加鎖,看看add()、clear()、remove()與get()方法的實現可能就有點眉目了。
首先我們可以看看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; // 將volatile Object[] array 的指向替換成新數組 setArray(newElements); return true; } finally { lock.unlock(); } }
通過代碼我們可以知道:在添加的時候就上鎖,并復制一個新數組,增加操作在新數組上完成,將array指向到新數組中,最后解鎖。
再來看看size()方法:
public int size() { // 直接得到array數組的長度 return getArray().length; }
再來看看get()方法:
public E get(int index) { return get(getArray(), index); } final Object[] getArray() { return array; }
那再來看看set()方法
public E set(int index, E element) { final ReentrantLock lock = this.lock; lock.lock(); try { // 得到原數組的舊值 Object[] elements = getArray(); E oldValue = get(elements, index); // 判斷新值和舊值是否相等 if (oldValue != element) { // 復制新數組,新值在新數組中完成 int len = elements.length; Object[] newElements = Arrays.copyOf(elements, len); newElements[index] = element; // 將array引用指向新數組 setArray(newElements); } else { // Not quite a no-op; enssures volatile write semantics setArray(elements); } return oldValue; } finally { lock.unlock(); } }
對于remove()、clear()跟set()和add()是類似的,這里我就不再貼出代碼了。
總結:
在修改時,復制出一個新數組,修改的操作在新數組中完成,最后將新數組交由array變量指向。
寫加鎖,讀不加鎖
2.1.3剖析為什么遍歷時不用調用者顯式加鎖常用的方法實現我們已經基本了解了,但還是不知道為啥能夠在容器遍歷的時候對其進行修改而不拋出異常。所以,來看一下他的迭代器吧:
// 1. 返回的迭代器是COWIterator public Iteratoriterator() { return new COWIterator (getArray(), 0); } // 2. 迭代器的成員屬性 private final Object[] snapshot; private int cursor; // 3. 迭代器的構造方法 private COWIterator(Object[] elements, int initialCursor) { cursor = initialCursor; snapshot = elements; } // 4. 迭代器的方法... public E next() { if (! hasNext()) throw new NoSuchElementException(); return (E) snapshot[cursor++]; } //.... 可以發現的是,迭代器所有的操作都基于snapshot數組,而snapshot是傳遞進來的array數組
到這里,我們應該就可以想明白了!CopyOnWriteArrayList在使用迭代器遍歷的時候,操作的都是原數組!
2.1.4CopyOnWriteArrayList缺點看了上面的實現源碼,我們應該也大概能分析出CopyOnWriteArrayList的缺點了。
內存占用:如果CopyOnWriteArrayList經常要增刪改里面的數據,經常要執行add()、set()、remove()的話,那是比較耗費內存的。
因為我們知道每次add()、set()、remove()這些增刪改操作都要復制一個數組出來。
數據一致性:CopyOnWrite容器只能保證數據的最終一致性,不能保證數據的實時一致性。
從上面的例子也可以看出來,比如線程A在迭代CopyOnWriteArrayList容器的數據。線程B在線程A迭代的間隙中將CopyOnWriteArrayList部分的數據修改了(已經調用setArray()了)。但是線程A迭代出來的是原有的數據。
2.1.5CopyOnWriteSetCopyOnWriteArraySet的原理就是CopyOnWriteArrayList。
private final CopyOnWriteArrayList三、最后al; public CopyOnWriteArraySet() { al = new CopyOnWriteArrayList (); }
現在臨近雙十一買阿里云服務器就特別省錢!之前我買學生機也要9.8塊錢一個月,現在最低價只需要8.3一個月!
如果有要買服務器的同學可通過我的鏈接直接享受最低價:https://m.aliyun.com/act/team1111/#/share?params=N.FF7yxCciiM.pfn5xpli
閱讀這篇文章可能需要對Java容器和多線程有一定的了解。如果對這些知識還不太了解的同學們可看我之前寫過的文章哦~
如果大家有更好的理解方式或者文章有錯誤的地方還請大家不吝在評論區留言,大家互相學習交流~~~
參考資料:
《Java并發編程實戰》
聊聊并發-Java中的Copy-On-Write容器:http://ifeve.com/java-copy-on-write/
Java 中的寫時復制 (Copy on Write, COW)https://juejin.im/post/5bc3065ce51d450e8e7758b5
擴展閱讀:
CopyOnWriteArrayList類set方法疑惑?http://ifeve.com/copyonwritearraylist-set/
Why setArray() method call required in CopyOnWriteArrayListhttps://stackoverflow.com/questions/28772539/why-setarray-method-call-required-in-copyonwritearraylist
一個堅持原創的Java技術公眾號:Java3y,歡迎大家關注
3y所有的原創文章:
文章的目錄導航(腦圖+海量視頻資源):https://github.com/ZhongFuCheng3y/3y
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://m.specialneedsforspecialkids.com/yun/72055.html
摘要:簡單來說是鏡像的源碼。例如,的鏡像鏡像,在中是一個基礎鏡像的鏡像也是鏡像那么鏡像和共享同一個基礎鏡像層,提高了存儲效率。 前言 只有光頭才能變強。 文本已收錄至我的GitHub倉庫,歡迎Star:https://github.com/ZhongFuCheng3y/3y showImg(https://segmentfault.com/img/remote/14600000180560...
摘要:我自己總結的學習的系統知識點以及面試問題,已經開源,目前已經。面試官那你都了解里面的哪些東西呢我哈哈哈這可是我的強項,從,說到,,又說到線程池,分別說了底層實現和項目中的應用。 我自己總結的Java學習的系統知識點以及面試問題,已經開源,目前已經 35k+ Star。會一直完善下去,歡迎建議和指導,同時也歡迎Star: https://github.com/Snailclimb... ...
摘要:而且只要他更新完畢對修飾的變量賦值,那么讀線程立馬可以看到最新修改后的數組,這是保證的。這個時候,就采用了思想來實現這個,避免更新的時候阻塞住高頻的讀操作,實現無鎖的效果,優化線程并發的性能。 今天聊一個非常硬核的技術知識,給大家分析一下CopyOnWrite思想是什么,以及在Java并發包中的具體體現,包括在Kafka內核源碼中是如何運用這個思想來優化并發性能的。這個CopyOnW...
摘要:準備不充分第一輪不過第一家,廣州琶洲一家環境超級好,福利也不錯,主營美顏的公司,這也是我最感遺憾的一次面試機會。主要是第一輪面試第一個問題的種數據類型,只答了一個。 前言 首先需要說明的一點,本人只是一個畢業一年,只有一年工作經驗的普通PHPer,能力有限,這篇文章只是將我這幾周來的感受和體驗分享出來,希望能給許多像我一樣,或者互聯網行業的新手帶來一些收獲,當然哪里說的不對或不足還是希...
摘要:技術一面一面主要考察基礎,有些會有技術筆試,比如騰訊,。騰訊的面試官就很喜歡問,安全,瀏覽器緩存方面的問題,計算機基礎,但是要懂為什么。 這篇文章簡單總結下2018年內我的一些前端面試經歷, 在這簡單分享一下,希望對大家有所啟發。 樓主在深圳,畢業兩年。面的主要是深圳的幾家公司。 包括: 騰訊, 螞蟻金服, Lazada, Shopee, 有贊 等 。 樓主在準備面試前, 想著復習一...
閱讀 3589·2019-08-30 15:55
閱讀 1384·2019-08-29 16:20
閱讀 3668·2019-08-29 12:42
閱讀 2673·2019-08-26 10:35
閱讀 1022·2019-08-26 10:23
閱讀 3420·2019-08-23 18:32
閱讀 909·2019-08-23 18:32
閱讀 2903·2019-08-23 14:55