摘要:體現(xiàn)的就是適配器模式。數(shù)組對象集合世界中的機制機制集合世界中比較常見的錯誤檢測機制,防止在對集合進(jìn)行遍歷過程當(dāng)中,出現(xiàn)意料之外的修改,會通過異常暴力的反應(yīng)出來。而在增強循環(huán)中,集合遍歷是通過進(jìn)行的。
前言
學(xué)習(xí)情況記錄
時間:week 2
SMART子目標(biāo) :Java 容器
記錄在學(xué)習(xí)Java容器 知識點中,關(guān)于List的重點知識點。
知識點概覽:
容器中的設(shè)計模式
從Arrays.asList() 看集合與數(shù)組的關(guān)系
集合世界中的 fail-fast 機制
什么是 fail-fast 機制
ArrayList.sublist() 有什么坑?
foreach 循環(huán)里為什么不能進(jìn)行元素的 remove/add 操作?
集合世界中的 fail-safe 機制
copy-on-write 機制
CopyOnWriteArrayList
關(guān)鍵知識點
讀寫操作
遍歷 - COWIterator
缺點 和 使用時需要注意的點
提問
容器中的設(shè)計模式 1.迭代器模式迭代器模式指的就是 提供一種方法順序訪問一個聚合對象中各個元素, 而又無須暴露該對象的內(nèi)部表示,為遍歷不同的聚合結(jié)構(gòu)提供一個統(tǒng)一的接口。
Collection 繼承了 Iterable 接口,其中的 iterator() 方法能夠產(chǎn)生一個 Iterator 對象,通過這個對象就可以迭代遍歷 Collection 中的元素。
從 JDK 1.5 之后可以使用foreach 方法來遍歷實現(xiàn)了 Iterable 接口的聚合對象。
2. 適配器模式適配器模式使得原本由于接口不兼容而不能一起工作的那些類可以一起工作。
Arrays.asList(T... a)體現(xiàn)的就是適配器模式。
拿生活中的例子作比方:我很早以前用的是3.5mm耳機孔的耳機,后面我換手機了,只能用type-c的耳機,通過type-c轉(zhuǎn)接頭我以前的耳機還是能用,這里面就用了適配器模式;在上面的例子中,入?yún)?shù)組就是3. 5mm耳機,Arrays.asList()這整個方法就是起到適配器type-c轉(zhuǎn)接頭的作用,List就是支持我type-c口的耳機。
從 Arrays.asList() 看集合與數(shù)組的關(guān)系(內(nèi)含坑)數(shù)組與集合都是用來存儲對象的容器,前者性質(zhì)單一、簡單易用;后者類型安全,功能強大,而兩者之間必然有相互轉(zhuǎn)換的方式。
由于兩者的特性存在很大的差別,所以在轉(zhuǎn)換過程當(dāng)中,如果不去詳細(xì)了解背后的轉(zhuǎn)換方式,很容易產(chǎn)生意料之外的問題。
在數(shù)組轉(zhuǎn)集合的過程中,需要注意是否使用了視圖方式。
這里說的視圖,指的就是一個具有限制的集合對象,只是把原有數(shù)據(jù)展現(xiàn)出來給你看,例如不可更改視圖,子視圖等等,這些視圖對于原對象具有不同的操作權(quán)限。
以 Arrays.asList() 為例,它把數(shù)組轉(zhuǎn)成集合時,不能修改其修改集合相關(guān)的內(nèi)容。它的add/remove/clear方法會拋出UnsupportedOperationException。
上述代碼可以證明可以通過set方法修改元素的值,原有數(shù)組相應(yīng)位置的值同時也會被修改,但是不能進(jìn)行修改元素個數(shù)的任何操作,否則就會拋異常。
有的人可能就會問了,返回的是ArrayList類,為什么不能對這個集合進(jìn)行修改呢?
因為這個ArrayList并不是我們平常使用的ArrayList類,這里是個冒牌貨,是Arrays工具類中的一個內(nèi)部類而已。
這個類非常的簡單,僅提供了改和查相關(guān)方法的實現(xiàn),讓我們來看一下:
至于增刪的操作會拋出會拋出UnsupportedOperationException,是在這個假類的父類AbstractList中實現(xiàn)的
所以當(dāng)你的業(yè)務(wù)場景中,數(shù)組轉(zhuǎn)成集合之后,如果可能會對集合進(jìn)行增和刪的操作,請使用真ArrayList來創(chuàng)建一個新集合。
List集合世界中的 fail-fast 機制
fail-fast 機制 集合世界中比較常見的錯誤檢測機制,防止在對集合進(jìn)行遍歷過程當(dāng)中,出現(xiàn)意料之外的修改,會通過Unchecked 異常暴力的反應(yīng)出來。
實現(xiàn)的方式就是:
當(dāng)前線程會維護(hù)一個計數(shù)比較器,即 expectedModCount,記錄已經(jīng)修改的次數(shù)。在進(jìn)入遍歷時,會把實時修改次數(shù) modCount賦值給 expectedModCount,如果這兩個數(shù)據(jù)不相等,則拋出異常。
java.util下的集合類都是屬于fail-fast的,而相對應(yīng)的,j.u.c下的集合類都是fail-safe,fail-safe在之后會介紹。
需要注意的是,即使不是多線程環(huán)境,如果單線程違反了規(guī)則,同樣也有可能會拋出改異常。比如ArrayList.subList()場景,比如foreach loop 中對集合進(jìn)行add/remove操作。
ArrayList.sublist() 有什么坑?subList()場景在《阿里開發(fā)手冊》上也是強制要求重點注意的一個規(guī)定。
List masterList = new ArrayList(); // ... 對 masterList 進(jìn)行一系列的set()操作,此處省略 List branchList = masterList.subList(0,3);
如上述場景,當(dāng)我們需要從一個主列表master中獲取子列表branch時,原集合元素個數(shù)的修改,會導(dǎo)致子列表的遍歷、增加、刪除均會產(chǎn)生ConcurrentModificationException。
foreach 循環(huán)里為什么不能進(jìn)行元素的 remove/add 操作?這也是《阿里開發(fā)手冊》中對集合處理的一個強制規(guī)約。
原因在于,foreach循環(huán)這樣的寫法,其實是Java本身給我們的一個語法糖,當(dāng)你對編譯之后class文件進(jìn)行反編譯之后,你會發(fā)現(xiàn),增強的for循環(huán),其實是依賴了while循環(huán)和Iterator實現(xiàn)的。
Iterator iterator = list.iterator(); do { if(!iterator.hasNext()) break; Object obj = iterator.next(); // 業(yè)務(wù)邏輯 瞎編的 if(canExecute()) { list.remove(object) } } while(true);
在增強for循環(huán)中,集合遍歷是通過iterator進(jìn)行的。
foreach循環(huán)這里要注意哦,你如果在foreach循環(huán)中調(diào)用了 集合的add/remove 方法,最后編譯出來的還是調(diào)用的邏輯是沒有變化的。
而在增強for循環(huán)中,集合遍歷是通過iterator進(jìn)行的。
沖突點就發(fā)生了,ArrayList 和 LinkedList中 add/remove方法的源碼中,雖然實現(xiàn)不一定相同,但是都會調(diào)用modCount++,這行代碼,當(dāng)你通過iterator進(jìn)行迭代時,每一次調(diào)用next()方法,都會調(diào)用一次checkForComodification()方法檢查集合在遍歷過程當(dāng)中被修改。
關(guān)鍵就在于集合自帶的add/remove方法不會去更新迭代器自身的expectedModCount值啊。
手冊里面為什么讓你使用Iterator的add/remove方法?因為除了調(diào)用對應(yīng)集合的對應(yīng)add/remove方法的同時,它還會去修改自身的expectedModCount值.
一言以蔽之,會拋出ConcurrentModificationException異常,是因為我們的代碼中使用了增強for循環(huán),而在增強for循環(huán)中,集合遍歷是通過iterator進(jìn)行的,但是元素的add/remove卻是直接使用的集合類自己的方法。這就導(dǎo)致iterator在遍歷的時候,會發(fā)現(xiàn)有一個元素在自己不知不覺的情況下就被刪除/添加了,就會拋出一個異常,用來提示用戶,可能發(fā)生了并發(fā)修改!
上述案例應(yīng)引起對刪除元素時的 fail-fast 警覺。我們可以使用Iterator機制進(jìn)行遍歷時的刪除,如果是多線程并發(fā)情況的話,還需要在Iterator遍歷時加鎖,如下源碼。
Iteratoriterator = list.iterator(); while(it.hasNext()) { synchronized(對象) { String item = iterator.next(); if (刪除元素的條件) { iterator.remove(); } } }
或者,可以直接使用JUC下對應(yīng)的線程安全集合,CopyOnWriteArrayList來代替。使用迭代器遍歷的時候就不用額外加鎖,也不會拋出ConcurrentModificationException異常。
集合世界中的 fail-safe 機制與 fail-fast 相對應(yīng)的,就是 fail-safe 機制;在J.U.C包中集合都是有這種機制實現(xiàn)的。
fail-safe 指的是:在安全的副本(或者沒有提供修改操作的正本)上進(jìn)行遍歷,集合修改和副本的遍歷是沒有任何關(guān)系的,但是缺點也很明顯,就是讀取不到最新的數(shù)據(jù)。
這也是 CAP 理論中 C (Consistency) 和 A (Availability) 的矛盾,即一致性與可用性之間的矛盾。
CAP 定理的含義 -- 阮一峰copy-on-write 機制
Copy-on-write 是解決并發(fā)的的一種思路,也是指的是實行讀寫分離,如果執(zhí)行的是寫操作,則復(fù)制一個新集合,在新集合內(nèi)添加或者刪除元素。待一切修改完成之后,再將原集合的引用指向新的集合。
這樣的好處就是,可以高并發(fā)地對COW進(jìn)行讀和遍歷操作,而不需要加鎖。因為當(dāng)前集合不會添加任何元素。
前面我們有提到過線程安全的集合Vector,但是Vector的加鎖粒度太大,性能差,所以在并發(fā)環(huán)境下,推薦JUC包下的的CopyOnWriteArrayList來代替。CopyOnWriteArrayList就是COW家族中的一員。
一般我們認(rèn)為,CopyOnWriteArrayList 是 同步List 的替代品,CopyOnWriteArraySet 是同步Set 的替代品。
By the way,關(guān)于寫時復(fù)制(copy-on-write)的這種思想,這種機制,并不是始于Java集合之中,在Linux、Redis、文件系統(tǒng)中都有相應(yīng)思想的設(shè)計,是一種計算機程序設(shè)計領(lǐng)域的優(yōu)化策略。
詳見本篇文章 COW奶牛!Copy On Write機制了解一下。
CopyOnWriteArrayList前面講的實際大多是概念性的東西,下面詳細(xì)剖析下CopyOnWriteArrayList ,讀一讀部分源碼,并且探討幾個在學(xué)習(xí)過程中的疑問。
關(guān)鍵知識點核心理念就是讀寫分離。
寫操作在一個復(fù)制的數(shù)組上進(jìn)行,讀操作還是在原始操作上進(jìn)行,讀寫分離,互不影響。
寫操作需要加鎖,防止并發(fā)寫入時導(dǎo)致數(shù)據(jù)丟失。
寫操作結(jié)束之后需要把 原始數(shù)組 指向新的復(fù)制數(shù)組。
讀寫操作以寫 - add() 方法 和 讀 - get() 方法為例
通過代碼我們可以知道:寫操作加鎖,防止并發(fā)寫入時導(dǎo)致數(shù)據(jù)丟失,并復(fù)制一個新數(shù)組,增加操作在新數(shù)組上完成,將array指向到新數(shù)組中,最后解鎖。
至于讀操作,則是直接讀取array數(shù)組中的元素。
遍歷 - COWIterator到現(xiàn)在,實際上還是沒有解釋為什么CopyOnWriteArrayList 在遍歷時,對其進(jìn)行修改而不拋出異常?
前面我們知道,不管是foreach 循環(huán)還是Iterator方式遍歷,實際上都是使用Iterator遍歷。那么就直接來看下CopyOnWriteArrayList 的iterator()方法。
public Iteratoriterator() { return new COWIterator (getArray(), 0); }
可以看到對應(yīng)的迭代器是COWIterator,看這個名字就可以知道這個是基于COW機制的,那么具體呢?
可以看到COWIterator的構(gòu)造方法,將集合的array數(shù)組傳入,實際上就是COWIterator內(nèi)部維護(hù)了一個對象指向集合的數(shù)組。
也就是說你使用COWIterator進(jìn)行遍歷的時候,如果你修改了集合,集合內(nèi)部的array就指向了新的一個數(shù)組對象,而COWIterator內(nèi)部的那個array還是指向初始化時傳進(jìn)來的舊數(shù)組,所以不會拋異常,因為舊數(shù)組永遠(yuǎn)沒變過。
缺點 和 使用時需要注意的點看完上面的解析,大概就能知道CopyOnWriteArrayList 在使用過程中的一些缺點了(實際上就是COW機制的缺點):
內(nèi)存占用:因為CopyOnWriteArrayList 的每次寫操作,都會復(fù)制一個新集合,所以如果對其進(jìn)行頻繁寫入,會在短時間內(nèi)造成大量的內(nèi)存占用。
數(shù)據(jù)一致性:這個前面提到過,再提一遍,CopyOnWrite容器只能保證數(shù)據(jù)的最終一致性,不能保證數(shù)據(jù)的實時一致性。
使用時注意的點:
盡量在讀多寫少的場景下去使用CopyOnWriteArrayList
盡量設(shè)置合理的容量初始值,因為擴(kuò)容代價大
使用批量刪除或批量添加方法,如addAll()或removeAll()操作,在高并發(fā)請求下,可以攢一下要添加或者刪除的元素,避免增加一個元素復(fù)制整個集合的情況
提問 Q: 為什么使用 final ReentrantLock lock = this.lock這樣的寫法?我在看CopyOnWriteArrayList 源碼的時候,發(fā)現(xiàn)寫操作相關(guān)的方法內(nèi)部,都是先將實例變量的lock對象引用賦值給方法的局部變量,然后再進(jìn)行鎖操作。
我那時候就納悶了很久,為什么要這么寫?直接調(diào)用實例中l(wèi)ock對象進(jìn)行鎖操作不是就可以了嗎?為什么要“多此一舉”呢?
查閱了Stack Overflow上相關(guān)的問題才知道,這實際上就是小小的性能優(yōu)化技巧。
理論上,訪問局部變量比訪問字段更快,也可能只占用更小的字節(jié)碼。 但是HotSpot編譯器實際上可以優(yōu)化對寄存器調(diào)用的字段訪問,所以這種寫法和直接訪問字段目前來說應(yīng)該沒有什么差別。btw,CopyOnWriteArrayList 是jdk1.5之后引進(jìn)的。體現(xiàn)了Doug Lea的性能優(yōu)化的極致追求。
實際上目前的JVM性能優(yōu)化的技術(shù),兩種寫法的性能已經(jīng)是沒有差別了。
在JDK11 中,這個實際上已經(jīng)無用的操作,已經(jīng)被刪去了。
最后本章的內(nèi)容到這里結(jié)束了,希望能對你有所幫助。如果有什么想要探討的隨時歡迎評論區(qū)留言。
參考《碼出高效》
《阿里巴巴Java開發(fā)手冊》
github cs-note
https://juejin.im/post/5c8717...
Why CopyOnWriteArrayList use getArray() to access an array reference?
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://systransis.cn/yun/75415.html
摘要:當(dāng)多個線程對同一個集合的內(nèi)容進(jìn)行操作時,就可能會產(chǎn)生事件。當(dāng)某一個線程遍歷的過程中,的內(nèi)容被另外一個線程所改變了就會拋出異常,產(chǎn)生事件。在線程在遍歷過程中的某一時刻,線程執(zhí)行了,并且線程刪除了中的節(jié)點。 概要 前面,我們已經(jīng)學(xué)習(xí)了ArrayList。接下來,我們以ArrayList為例,對Iterator的fail-fast機制進(jìn)行了解。 1 fail-fast簡介 fail-fast...
摘要:包含兩個重要的成員和。對于多線程環(huán)境,且可能同時被多個線程操作,此時,應(yīng)該使用同步的類如。小于等于且大于,代表用戶創(chuàng)建了一個,但是使用的構(gòu)造函數(shù)為或或,導(dǎo)致為,為,為用戶指定的的初始容量。本質(zhì)上是數(shù)組單向鏈表紅黑樹的數(shù)據(jù)結(jié)構(gòu)如下圖。 一、List 1、ArrayList ① 關(guān)鍵源碼 // 默認(rèn)初始化為空數(shù)組 public ArrayList() { this.elementD...
摘要:注意,迭代器的快速失敗行為無法得到保證,因為一般來說,不可能對是否出現(xiàn)不同步并發(fā)修改做出任何硬性保證??焖偈〉鲿M最大努力拋出。 fail-fast與fail-safe 在Collection集合的各個類中,有線程安全和線程不安全這2大類的版本。 對于線程不安全的類,并發(fā)情況下可能會出現(xiàn)fail-fast情況;而線程安全的類,可能出現(xiàn)fail-safe的情況。 一、并發(fā)修改 當(dāng)一...
摘要:而在集合中,值僅僅是一個對象罷了該對象對本身而言是無用的。將這篇文章作為集合的總結(jié)篇,但覺得沒什么好寫就回答一些面試題去了,找了一會面試題又覺得不夠系統(tǒng)。 前言 聲明,本文用的是jdk1.8 花了一個星期,把Java容器核心的知識過了一遍,感覺集合已經(jīng)無所畏懼了??!(哈哈哈....),現(xiàn)在來總結(jié)一下吧~~ 回顧目錄: Collection總覽 List集合就這么簡單【源碼剖析】 Ma...
摘要:與在迭代器中的設(shè)計在中,最典型的與就是關(guān)于迭代器的設(shè)計。缺點是,迭代器不能正確及時的反應(yīng)集合中的內(nèi)容,而且一定程度上也增加了內(nèi)存的消耗。 fail-fast與fail-safe簡介 如果一個系統(tǒng),當(dāng)有異常或者錯誤發(fā)生時就立即中斷執(zhí)行,這種設(shè)計稱之為fail-fast。相反如果我們的系統(tǒng)可以在某種異?;蛘咤e誤發(fā)生時繼續(xù)執(zhí)行,不會被中斷,這種設(shè)計稱之為fail-safe。 fail-fas...
閱讀 1269·2021-11-19 09:40
閱讀 3128·2021-11-02 14:47
閱讀 3101·2021-10-11 10:58
閱讀 3224·2019-08-30 15:54
閱讀 2678·2019-08-30 12:50
閱讀 1732·2019-08-29 16:54
閱讀 473·2019-08-29 15:38
閱讀 1243·2019-08-29 15:19