Java多線程環(huán)境下使用的集合類示例詳解
前言
Java標準庫中大部分集合類都是線程不安全的, 多線程環(huán)境下使用同一個集合類對象, 很可能會出問題; 只有少部分是線程安全的, 比如: Vector, Stack, HashTable這些, 關(guān)鍵方法都會帶有 synchronized, 但一般是不推薦使用這幾個類的.
一. 多線程環(huán)境下使用ArrayList
ArrayList在多線程中是線程不安全的, 使用時要保證線程安全的話有如下幾種方式:
- 涉及線程安全問題的代碼中, 自己使用synchronized或者ReentrantLock進行加鎖.
- 使用標準庫里面的操作: Collections.synchronizedList(new ArrayList); synchronizedList中的關(guān)鍵操作上都帶有synchronized的.
- 使用基于寫實拷貝實現(xiàn)的CopyOnWriteArrayList.
所謂寫實拷貝就是, 如果針對該ArrayList進行讀操作, 不會做任何額外的工作, 因為只有只有讀操作的話是不涉及線程安全問題的;
如果進行寫操作則拷貝一份新的ArrayList, 然后針對新的ArrayList進行修改, 如果在寫過程中有讀操作, 那么就去讀舊ArrayList, 當我們新的ArrayList寫操作完成之后, 就讓新的替換舊的ArrayList(本質(zhì)上就是一個引用的賦值, 是原子的).
很明顯, 這里寫時拷貝的方案, 優(yōu)點是在讀多寫少的場景下, 性能很高, 不需要加鎖競爭; 缺點是這種方案占用內(nèi)存較多, 要求這個ArrayList不能太大, 而且新寫的數(shù)據(jù)不能被第一時間讀取到.
??寫時拷貝的應用:
這種寫時拷貝的思路可以應用在服務(wù)器的 “熱加載” (reload) 這樣的功能上.
在服務(wù)器程序中, 包含有很多的子功能, 有的功能想要使用, 有的不想要使用, 有的希望功能應用不同, 所以可以使用一系列的 “開關(guān)選項” 來控制當前這個程序的工作狀態(tài), 這里就涉及到服務(wù)器程序配置文件的修改, 修改配置后可能就需要重啟服務(wù)器才能生效, 但是重啟操作可能成本比較高.
為什么說成本比較高呢?
假設(shè)一個服務(wù)器重啟需要花5min(往小了說的), 如果有20臺這樣的服務(wù)器, 總的重啟時間就得100min, 注意這20臺服務(wù)器是不能一起重啟的, 一起重啟就意味著在這5min中所有的服務(wù)器都不工作了, 服務(wù)就中斷了, 用戶發(fā)起的請求就沒有了任何的響應, 這個狀況就是比較嚴重的事故了.
而使用 “熱加載” 這樣功能就可以不重啟服務(wù)器實現(xiàn)配置的更新, 可以利用寫時拷貝的思路, 新的配置放到新的對象中, 加載過程中, 請求仍然基于舊配置進行工作, 當新的對象加載完畢, 就使用新對象替代舊對象(替換完成之后,舊的對象就可以釋放了).
二. 多線程環(huán)境使用隊列
多線程環(huán)境下通常使用的是阻塞隊列:
ArrayBlockingQueue基于數(shù)組實現(xiàn)的阻塞隊列.LinkedBlockingQueue基于鏈表實現(xiàn)的阻塞隊列.PriorityBlockingQueue基于堆實現(xiàn)的帶優(yōu)先級的阻塞隊列.TransferQueue最多只包含一個元素的阻塞隊列.
三. 多線程環(huán)境下使用哈希表
HashMap本身是線程不安全的, 將HashMap中的重要方法使用synchornized加鎖后, 就得到了Hashtable類, Hashtable是線程安全的, 但相比于Hashtable更推薦使用的是ConcurrentHashMap.
那么下面就來分析一下, ConcurrentHashMap相較于Hashtable進行了哪些優(yōu)化.
??1. 最大的優(yōu)化: ConcurrentHashMap相較于Hashtable大大縮小了鎖沖突的概率, 將一把大鎖換為多把小鎖.
首先看線程不安全的HashMap, 假設(shè)表中如下圖, 這里元素1, 2是在同一個鏈表上, 如果線程A修改(增/刪/改)元素1, 線程B修改(增/刪/改)元素2, 這里是存在線程安全問題的, 1和2位置這是兩個相鄰的節(jié)點, 如果此時并發(fā)的插入/刪除, 就需要修改這兩節(jié)點next的指向, 所以這個情況要解決是需要加鎖的.
再來看如果線程A修改元素3, 線程B修改元素4, 這里是沒有線程安全問題的, 3和4位置的節(jié)點是位于兩個不同的鏈表中的, 這個情況就相當于多個線程并發(fā)去修改不同的變量, 是不存在線程安全問題的, 隨之這里也就是不需要加鎖了.

上面說到的HashMap是有線程安全問題的, 而針對上面的場景使用Hashtable是可以解決線程安全問題的, 但Hashtable中不太必要的是它是直接在方法上加synchronized, 等于是是給this加鎖, 這把大鎖就導致了我們只要操作哈希表上的任意元素, 都會加鎖, 也就都可能會發(fā)生鎖沖突.

但是實際上, 基于哈希表的結(jié)構(gòu)特點, 有些元素在進行并發(fā)操作的時候, 是不會產(chǎn)生線程安全問題的, 也就沒必要去加鎖控制了, 就如上面分析過的3和4位置的元素.
這就是不推薦使用Hashtable的主要原因, 這里鎖沖突概率太大了, 就勢必會造成一些并發(fā)效率低下的問題.

而ConcurrentHashMap的做法是每個鏈表有各自的鎖, 就不是像Hashtable一樣大家共用一個鎖了, 具體來說, 就是使用每個鏈表的頭結(jié)點, 作為鎖對象, 這樣只有并發(fā)操作同一個鏈表中的元素才會有鎖競爭, 大大降低了鎖沖突的概率, 相較于Hashtable并發(fā)效率上也會提升不少.
相較于Hashtable, 這里就是把鎖的粒度變小了, 不同線程操作1和2時, 是針對同一把鎖進行加鎖, 會產(chǎn)生鎖競爭, 保證了線程安全; 而不同線程操作3和4時, 是針對不同的鎖進行加鎖, 所以不會產(chǎn)生鎖競爭.

上圖中的情況, 是針對JDK1.8及其以后的情況, 而JDK1.8之前, ConcurrentHashMap使用的是 “分段鎖”, 分段鎖本質(zhì)上也是縮小鎖的范圍從而降低鎖沖突的概率, 但是這種做法不夠徹底, 一方面鎖的粒度切分的還不夠細, 另一方面代碼實現(xiàn)也更繁瑣.

??2. ConcurrentHashMap做了一個激進的操作, 只針對寫操作加鎖, 而針對讀操作不加鎖, 這里讀和讀之間沒有沖突, 寫和寫之間有沖突, 而讀和寫之間也沒有沖突是為什么呢?
在很多場景下, 讀寫之間不加鎖控制,可能會讀到一個寫了一半的結(jié)果, 如果寫操作不是原子的, 此時讀就可能會讀到寫了一半的數(shù)據(jù), 相當于臟讀了, 而這里的寫操作使用了volatile來保證我們每次都是從內(nèi)存讀取的結(jié)果并且寫操作加鎖保證了寫操作是原子的, 這樣就沒有上述說到的問題了.
??3. ConcurrentHashMap內(nèi)部充分利用CAS特性, 來減少加鎖的操作, 比如通過CAS來維護size屬性(元素個數(shù)).
??4. 針對擴容操作, 采取了"化整為零"的策略.
HashMap和Hashtable中的擴容, 是直接創(chuàng)建一個空間更大新數(shù)組, 然后將舊的數(shù)組上的每個元素搬到新數(shù)組上(刪除節(jié)點+插入節(jié)點), 當我們某一次進行put時, 元素個數(shù)達到負載因子設(shè)定的值, 就會觸發(fā)擴容操作, 此時如果哈希表中的元素特別多, 擴容操作就會比較耗時, 也就是某次put比平時的put卡很多倍.
而在ConcurrentHashMap中采取擴容方式是每次只搬運─小部分元素, 具體來說就是, 擴容時創(chuàng)建一個新的數(shù)組, 舊的數(shù)組也會保留下來, 之后的每次put操作直接往新數(shù)組上添加, 同時搬運一部分舊的元素到新數(shù)組上, 當進行get操作時, 新舊數(shù)組都進行查詢, 當進行remove操作時, 元素在哪個數(shù)組上正常進行刪除操作即可, 這樣經(jīng)過一段時間之后, 當所有元素都搬運到了性數(shù)組上, 然后再釋放舊數(shù)組即可.
總結(jié):
- HashMap: 線程不安全, key 允許為 null.
- Hashtable: 線程安全, 使用 synchronized 鎖 Hashtable 對象, 效率較低, key 不允許為 null.
- ConcurrentHashMap: 線程安全, 使用 synchronized 鎖每個鏈表頭結(jié)點, 鎖沖突概率低, 充分利用 CAS 機制, 優(yōu)化了擴容方式, key 不允許為 null.
到此這篇關(guān)于Java多線程環(huán)境下使用的集合類的文章就介紹到這了,更多相關(guān)Java多線程使用集合類內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
java基于QuartzJobBean實現(xiàn)定時功能的示例代碼
QuartzJobBean是Quartz框架中的一個抽象類,用于定義和實現(xiàn)可由Quartz調(diào)度的作業(yè),本文主要介紹了java基于QuartzJobBean實現(xiàn)定時功能的示例代碼,具有一定的參考價值,感興趣可以了解一下2023-09-09
Vue?+?springboot實現(xiàn)拼圖人機驗證功能
本文介紹了如何使用Vue和Spring?Boot實現(xiàn)拼圖人機驗證功能,本文通過實例代碼給大家介紹的非常詳細,對大家的學習或工作具有一定的參考借鑒價值,需要的朋友參考下吧2026-01-01
SpringBoot實現(xiàn)應用開機自啟動與進程守護配置方案
機房意外斷電,恢復供電后所有服務(wù)器重啟,但SpringBoot應用沒有自動啟動,事后排查發(fā)現(xiàn),沒有留下開機自啟動配置文檔,所以本文整理了兩套SpringBoot應用開機自啟動與進程守護方案,需要的朋友可以參考下2025-07-07
Java利用FTPClient實現(xiàn)文件上傳和下載的實踐指南
本文介紹如何使用Java中的FTPClient庫來執(zhí)行文件的上傳和下載操作,首先,講解了FTPClient的基本用法,包括連接FTP服務(wù)器和登錄驗證,然后,詳細說明了如何使用 storeFile() 和 retrieveFile() 方法分別實現(xiàn)文件上傳和下載,需要的朋友可以參考下2025-05-05
SpringBoot 中html的頁面間跳轉(zhuǎn)問題小結(jié)
這篇文章主要介紹了SpringBoot 中html的頁面間跳轉(zhuǎn)問題小結(jié),本文給大家分享兩種方法,結(jié)合實例代碼給大家介紹的非常詳細,需要的朋友可以參考下2023-10-10

