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