Java多線程環(huán)境下使用的集合類示例詳解
前言
Java標(biāo)準(zhǔn)庫中大部分集合類都是線程不安全的, 多線程環(huán)境下使用同一個(gè)集合類對(duì)象, 很可能會(huì)出問題; 只有少部分是線程安全的, 比如: Vector
, Stack
, HashTable
這些, 關(guān)鍵方法都會(huì)帶有 synchronized
, 但一般是不推薦使用這幾個(gè)類的.
一. 多線程環(huán)境下使用ArrayList
ArrayList在多線程中是線程不安全的, 使用時(shí)要保證線程安全的話有如下幾種方式:
- 涉及線程安全問題的代碼中, 自己使用synchronized或者ReentrantLock進(jìn)行加鎖.
- 使用標(biāo)準(zhǔn)庫里面的操作: Collections.synchronizedList(new ArrayList); synchronizedList中的關(guān)鍵操作上都帶有synchronized的.
- 使用基于寫實(shí)拷貝實(shí)現(xiàn)的CopyOnWriteArrayList.
所謂寫實(shí)拷貝就是, 如果針對(duì)該ArrayList進(jìn)行讀操作, 不會(huì)做任何額外的工作, 因?yàn)橹挥兄挥凶x操作的話是不涉及線程安全問題的;
如果進(jìn)行寫操作則拷貝一份新的ArrayList, 然后針對(duì)新的ArrayList進(jìn)行修改, 如果在寫過程中有讀操作, 那么就去讀舊ArrayList, 當(dāng)我們新的ArrayList寫操作完成之后, 就讓新的替換舊的ArrayList(本質(zhì)上就是一個(gè)引用的賦值, 是原子的).
很明顯, 這里寫時(shí)拷貝的方案, 優(yōu)點(diǎn)是在讀多寫少的場景下, 性能很高, 不需要加鎖競爭; 缺點(diǎn)是這種方案占用內(nèi)存較多, 要求這個(gè)ArrayList不能太大, 而且新寫的數(shù)據(jù)不能被第一時(shí)間讀取到.
??寫時(shí)拷貝的應(yīng)用:
這種寫時(shí)拷貝的思路可以應(yīng)用在服務(wù)器的 “熱加載” (reload) 這樣的功能上.
在服務(wù)器程序中, 包含有很多的子功能, 有的功能想要使用, 有的不想要使用, 有的希望功能應(yīng)用不同, 所以可以使用一系列的 “開關(guān)選項(xiàng)” 來控制當(dāng)前這個(gè)程序的工作狀態(tài), 這里就涉及到服務(wù)器程序配置文件的修改, 修改配置后可能就需要重啟服務(wù)器才能生效, 但是重啟操作可能成本比較高.
為什么說成本比較高呢?
假設(shè)一個(gè)服務(wù)器重啟需要花5min
(往小了說的), 如果有20
臺(tái)這樣的服務(wù)器, 總的重啟時(shí)間就得100min
, 注意這20
臺(tái)服務(wù)器是不能一起重啟的, 一起重啟就意味著在這5min
中所有的服務(wù)器都不工作了, 服務(wù)就中斷了, 用戶發(fā)起的請(qǐng)求就沒有了任何的響應(yīng), 這個(gè)狀況就是比較嚴(yán)重的事故了.
而使用 “熱加載” 這樣功能就可以不重啟服務(wù)器實(shí)現(xiàn)配置的更新, 可以利用寫時(shí)拷貝的思路, 新的配置放到新的對(duì)象中, 加載過程中, 請(qǐng)求仍然基于舊配置進(jìn)行工作, 當(dāng)新的對(duì)象加載完畢, 就使用新對(duì)象替代舊對(duì)象(替換完成之后,舊的對(duì)象就可以釋放了).
二. 多線程環(huán)境使用隊(duì)列
多線程環(huán)境下通常使用的是阻塞隊(duì)列:
ArrayBlockingQueue
基于數(shù)組實(shí)現(xiàn)的阻塞隊(duì)列.LinkedBlockingQueue
基于鏈表實(shí)現(xiàn)的阻塞隊(duì)列.PriorityBlockingQueue
基于堆實(shí)現(xiàn)的帶優(yōu)先級(jí)的阻塞隊(duì)列.TransferQueue
最多只包含一個(gè)元素的阻塞隊(duì)列.
三. 多線程環(huán)境下使用哈希表
HashMap
本身是線程不安全的, 將HashMap中的重要方法使用synchornized加鎖后, 就得到了Hashtable類, Hashtable
是線程安全的, 但相比于Hashtable更推薦使用的是ConcurrentHashMap
.
那么下面就來分析一下, ConcurrentHashMap
相較于Hashtable
進(jìn)行了哪些優(yōu)化.
??1. 最大的優(yōu)化: ConcurrentHashMap相較于Hashtable大大縮小了鎖沖突的概率, 將一把大鎖換為多把小鎖.
首先看線程不安全的HashMap
, 假設(shè)表中如下圖, 這里元素1, 2
是在同一個(gè)鏈表上, 如果線程A修改(增/刪/改)元素1
, 線程B修改(增/刪/改)元素2
, 這里是存在線程安全問題的, 1和2
位置這是兩個(gè)相鄰的節(jié)點(diǎn), 如果此時(shí)并發(fā)的插入/刪除, 就需要修改這兩節(jié)點(diǎn)next
的指向, 所以這個(gè)情況要解決是需要加鎖的.
再來看如果線程A修改元素3
, 線程B修改元素4
, 這里是沒有線程安全問題的, 3和4
位置的節(jié)點(diǎn)是位于兩個(gè)不同的鏈表中的, 這個(gè)情況就相當(dāng)于多個(gè)線程并發(fā)去修改不同的變量, 是不存在線程安全問題的, 隨之這里也就是不需要加鎖了.
上面說到的HashMap是有線程安全問題的, 而針對(duì)上面的場景使用Hashtable
是可以解決線程安全問題的, 但Hashtable中不太必要的是它是直接在方法上加synchronized
, 等于是是給this
加鎖, 這把大鎖就導(dǎo)致了我們只要操作哈希表上的任意元素, 都會(huì)加鎖, 也就都可能會(huì)發(fā)生鎖沖突.
但是實(shí)際上, 基于哈希表的結(jié)構(gòu)特點(diǎn), 有些元素在進(jìn)行并發(fā)操作的時(shí)候, 是不會(huì)產(chǎn)生線程安全問題的, 也就沒必要去加鎖控制了, 就如上面分析過的3和4
位置的元素.
這就是不推薦使用Hashtable的主要原因, 這里鎖沖突概率太大了, 就勢必會(huì)造成一些并發(fā)效率低下的問題.
而ConcurrentHashMap
的做法是每個(gè)鏈表有各自的鎖, 就不是像Hashtable一樣大家共用一個(gè)鎖了, 具體來說, 就是使用每個(gè)鏈表的頭結(jié)點(diǎn), 作為鎖對(duì)象, 這樣只有并發(fā)操作同一個(gè)鏈表中的元素才會(huì)有鎖競爭, 大大降低了鎖沖突的概率, 相較于Hashtable并發(fā)效率上也會(huì)提升不少.
相較于Hashtable, 這里就是把鎖的粒度變小了, 不同線程操作1和2
時(shí), 是針對(duì)同一把鎖進(jìn)行加鎖, 會(huì)產(chǎn)生鎖競爭, 保證了線程安全; 而不同線程操作3和4
時(shí), 是針對(duì)不同的鎖進(jìn)行加鎖, 所以不會(huì)產(chǎn)生鎖競爭.
上圖中的情況, 是針對(duì)JDK1.8
及其以后的情況, 而JDK1.8
之前, ConcurrentHashMap
使用的是 “分段鎖”, 分段鎖本質(zhì)上也是縮小鎖的范圍從而降低鎖沖突的概率, 但是這種做法不夠徹底, 一方面鎖的粒度切分的還不夠細(xì), 另一方面代碼實(shí)現(xiàn)也更繁瑣.
??2. ConcurrentHashMap
做了一個(gè)激進(jìn)的操作, 只針對(duì)寫操作加鎖, 而針對(duì)讀操作不加鎖, 這里讀和讀之間沒有沖突, 寫和寫之間有沖突, 而讀和寫之間也沒有沖突是為什么呢?
在很多場景下, 讀寫之間不加鎖控制,可能會(huì)讀到一個(gè)寫了一半的結(jié)果, 如果寫操作不是原子的, 此時(shí)讀就可能會(huì)讀到寫了一半的數(shù)據(jù), 相當(dāng)于臟讀了, 而這里的寫操作使用了volatile
來保證我們每次都是從內(nèi)存讀取的結(jié)果并且寫操作加鎖保證了寫操作是原子的, 這樣就沒有上述說到的問題了.
??3. ConcurrentHashMap
內(nèi)部充分利用CAS
特性, 來減少加鎖的操作, 比如通過CAS
來維護(hù)size屬性(元素個(gè)數(shù)).
??4. 針對(duì)擴(kuò)容操作, 采取了"化整為零"的策略.
HashMap
和Hashtable
中的擴(kuò)容, 是直接創(chuàng)建一個(gè)空間更大新數(shù)組, 然后將舊的數(shù)組上的每個(gè)元素搬到新數(shù)組上(刪除節(jié)點(diǎn)+插入節(jié)點(diǎn)), 當(dāng)我們某一次進(jìn)行put
時(shí), 元素個(gè)數(shù)達(dá)到負(fù)載因子設(shè)定的值, 就會(huì)觸發(fā)擴(kuò)容操作, 此時(shí)如果哈希表中的元素特別多, 擴(kuò)容操作就會(huì)比較耗時(shí), 也就是某次put
比平時(shí)的put
卡很多倍.
而在ConcurrentHashMap
中采取擴(kuò)容方式是每次只搬運(yùn)─小部分元素, 具體來說就是, 擴(kuò)容時(shí)創(chuàng)建一個(gè)新的數(shù)組, 舊的數(shù)組也會(huì)保留下來, 之后的每次put
操作直接往新數(shù)組上添加, 同時(shí)搬運(yùn)一部分舊的元素到新數(shù)組上, 當(dāng)進(jìn)行get
操作時(shí), 新舊數(shù)組都進(jìn)行查詢, 當(dāng)進(jìn)行remove
操作時(shí), 元素在哪個(gè)數(shù)組上正常進(jìn)行刪除操作即可, 這樣經(jīng)過一段時(shí)間之后, 當(dāng)所有元素都搬運(yùn)到了性數(shù)組上, 然后再釋放舊數(shù)組即可.
總結(jié):
- HashMap: 線程不安全, key 允許為 null.
- Hashtable: 線程安全, 使用 synchronized 鎖 Hashtable 對(duì)象, 效率較低, key 不允許為 null.
- ConcurrentHashMap: 線程安全, 使用 synchronized 鎖每個(gè)鏈表頭結(jié)點(diǎn), 鎖沖突概率低, 充分利用 CAS 機(jī)制, 優(yōu)化了擴(kuò)容方式, key 不允許為 null.
到此這篇關(guān)于Java多線程環(huán)境下使用的集合類的文章就介紹到這了,更多相關(guān)Java多線程使用集合類內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Spring中自定義數(shù)據(jù)類型轉(zhuǎn)換的方法詳解
Spring3引入了一個(gè)core.onvert包,提供一個(gè)通用類型轉(zhuǎn)換系統(tǒng)。在Spring容器中,可以使用這個(gè)系統(tǒng)作為PropertyEditor實(shí)現(xiàn)的替代,將外部化的bean屬性值字符串轉(zhuǎn)換為所需的屬性類型。本文將詳解這一系統(tǒng)的使用方法,需要的可以參考一下2022-06-06IntelliJ IDEA的數(shù)據(jù)庫管理工具實(shí)在太方便了(推薦)
這篇文章主要介紹了IntelliJ IDEA的數(shù)據(jù)庫管理工具實(shí)在太方便了,本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2020-09-09Feign遠(yuǎn)程調(diào)用傳遞對(duì)象參數(shù)并返回自定義分頁數(shù)據(jù)的過程解析
這篇文章主要介紹了Feign遠(yuǎn)程調(diào)用傳遞對(duì)象參數(shù)并返回自定義分頁數(shù)據(jù)的過程解析,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-03-03Mybatis使用typeHandler加密的實(shí)現(xiàn)
本文詳細(xì)介紹了如何在Mybatis中使用typeHandler對(duì)特定字段進(jìn)行加密處理,涵蓋了從引入依賴、配置Mybatis,到實(shí)現(xiàn)typeHandler繼承類和配置mapper層的詳細(xì)步驟,為需要在項(xiàng)目中實(shí)現(xiàn)字段加密的開發(fā)者提供了參考和借鑒2024-09-09Java中字符數(shù)組、String類、StringBuffer三者之間相互轉(zhuǎn)換
這篇文章主要介紹了Java中字符數(shù)組、String類、StringBuffer三者之間相互轉(zhuǎn)換,需要的朋友可以參考下2018-05-05java使用WatchService監(jiān)控文件夾示例
本篇文章主要介紹了java使用WatchService監(jiān)控文件夾示例的資料,這里整理了詳細(xì)的代碼,有需要的小伙伴可以參考下。2017-02-02java實(shí)現(xiàn)ArrayList根據(jù)存儲(chǔ)對(duì)象排序功能示例
這篇文章主要介紹了java實(shí)現(xiàn)ArrayList根據(jù)存儲(chǔ)對(duì)象排序功能,結(jié)合實(shí)例形式分析了java針對(duì)ArrayList的相關(guān)運(yùn)算、排序操作技巧,需要的朋友可以參考下2018-01-01Java基于自定義類加載器實(shí)現(xiàn)熱部署過程解析
這篇文章主要介紹了Java基于自定義類加載器實(shí)現(xiàn)熱部署過程解析,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-03-03