java中在多線程的情況下安全的修改list(常見解決方案)
在Java中,ArrayList
、LinkedList
等常見List
實現(xiàn)類不是線程安全的(非同步)。當多個線程同時對其進行修改(如add
、remove
)或讀寫操作時,可能會導致數(shù)據(jù)不一致、ConcurrentModificationException
(并發(fā)修改異常)等問題。
要在多線程環(huán)境下安全地修改List
,需通過線程安全的容器或同步機制保證操作的原子性和可見性。以下是常用解決方案及實現(xiàn)方式:
一、使用線程安全的List實現(xiàn)類
Java提供了幾種線程安全的List
實現(xiàn),可直接替換非線程安全的List
,無需手動處理同步。
1.Vector(古老實現(xiàn),不推薦)
Vector
是Java早期的線程安全List
實現(xiàn),其所有方法都被synchronized
修飾(同步方法),保證線程安全。
缺點:同步粒度太粗(整個方法加鎖),多線程并發(fā)效率低,且功能上被更優(yōu)的方案替代,不推薦在新代碼中使用。
// Vector是線程安全的,但性能較差 List<String> vector = new Vector<>(); // 多線程可安全調(diào)用add/remove等方法 vector.add("A"); vector.remove(0);
2.Collections.synchronizedList()(包裝同步,推薦基礎場景)
Collections
工具類的synchronizedList()
方法可將任意非線程安全的List
包裝為線程安全的List
。其原理是對所有方法添加同步鎖(使用synchronized
塊),保證同一時刻只有一個線程能操作List
。
使用方式:
// 1. 創(chuàng)建非線程安全的List(如ArrayList) List<String> unsafeList = new ArrayList<>(); // 2. 包裝為線程安全的List List<String> safeList = Collections.synchronizedList(unsafeList); // 多線程環(huán)境下可安全操作 // 線程1:添加元素 new Thread(() -> { safeList.add("A"); }).start(); // 線程2:刪除元素 new Thread(() -> { if (!safeList.isEmpty()) { safeList.remove(0); } }).start();
注意事項:
迭代操作需手動加鎖:synchronizedList
返回的List
在迭代時(如for-each
、iterator
)不自動同步,需手動用synchronized
塊包裹,否則可能拋出ConcurrentModificationException
。
// 迭代時必須手動同步(鎖對象為safeList本身) synchronized (safeList) { for (String s : safeList) { System.out.println(s); } }
適合讀寫頻率均衡的場景:由于所有操作都加鎖,高并發(fā)下性能一般,但實現(xiàn)簡單,適合大多數(shù)基礎場景。
3.CopyOnWriteArrayList(寫時復制,推薦讀多寫少場景)
CopyOnWriteArrayList
是Java并發(fā)包(java.util.concurrent
)提供的線程安全List
,其核心原理是**“寫時復制”**:
- 讀操作:無需加鎖,直接訪問當前數(shù)組(性能極高)。
- 寫操作(
add
、remove
等):先復制一份新的數(shù)組,在新數(shù)組上修改,然后將引用指向新數(shù)組(修改時加鎖,保證原子性)。
適用場景:讀操作遠多于寫操作(如緩存、配置列表),寫操作頻率低但讀操作需高效。
使用方式:
import java.util.concurrent.CopyOnWriteArrayList; // 初始化線程安全的CopyOnWriteArrayList List<String> cowList = new CopyOnWriteArrayList<>(); // 多線程安全操作 // 線程1:添加元素(寫操作,會復制數(shù)組) new Thread(() -> { cowList.add("A"); }).start(); // 線程2:讀取元素(讀操作,無鎖,直接訪問) new Thread(() -> { for (String s : cowList) { System.out.println(s); } }).start();
優(yōu)點:
- 讀操作無鎖,并發(fā)性能極佳(適合讀多寫少)。
- 迭代時不會拋出
ConcurrentModificationException
(迭代的是舊數(shù)組的快照)。
缺點:
- 寫操作成本高(復制數(shù)組,內(nèi)存占用翻倍)。
- 數(shù)據(jù)實時性差(讀操作可能訪問的是舊數(shù)組,修改后的數(shù)據(jù)需等新數(shù)組替換后才能被讀?。?。
二、手動同步(鎖機制)
如果需要更靈活地控制同步粒度(如僅對關鍵修改操作加鎖),可使用synchronized
關鍵字或Lock
接口手動實現(xiàn)同步。
1. 使用synchronized塊
通過synchronized
鎖定List
對象或其他鎖對象,保證同一時刻只有一個線程執(zhí)行修改操作。
List<String> list = new ArrayList<>(); // 定義鎖對象(也可直接用list本身作為鎖) Object lock = new Object(); // 線程1:添加元素 new Thread(() -> { synchronized (lock) { // 加鎖 list.add("A"); } }).start(); // 線程2:刪除元素 new Thread(() -> { synchronized (lock) { // 加鎖 if (!list.isEmpty()) { list.remove(0); } } }).start();
2. 使用ReentrantLock(可重入鎖)
java.util.concurrent.locks.ReentrantLock
提供比synchronized
更靈活的鎖控制(如超時鎖、公平鎖),適合復雜場景。
import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; List<String> list = new ArrayList<>(); // 創(chuàng)建鎖對象(可指定為公平鎖,按請求順序獲取鎖) Lock lock = new ReentrantLock(true); // 線程1:添加元素 new Thread(() -> { lock.lock(); // 加鎖 try { list.add("A"); } finally { lock.unlock(); // 必須在finally中釋放鎖,避免死鎖 } }).start(); // 線程2:刪除元素 new Thread(() -> { lock.lock(); // 加鎖 try { if (!list.isEmpty()) { list.remove(0); } } finally { lock.unlock(); } }).start();
三、注意事項
- 復合操作的原子性:
- 即使使用線程安全的
List
,復合操作(如“先判斷再修改”)仍需額外同步。例如:
- 即使使用線程安全的
// 錯誤示例:contains和add是兩個獨立操作,可能被其他線程打斷 if (!safeList.contains("A")) { safeList.add("A"); // 可能重復添加 } // 正確:用同步塊保證復合操作原子性 synchronized (safeList) { if (!safeList.contains("A")) { safeList.add("A"); } }
- 迭代器的線程安全:
synchronizedList
的迭代器需手動同步(見上文)。CopyOnWriteArrayList
的迭代器是“快照迭代器”,不支持remove
、add
等修改操作(會拋UnsupportedOperationException
),只能遍歷。
- 性能權(quán)衡:
- 讀多寫少:優(yōu)先
CopyOnWriteArrayList
(讀無鎖)。 - 讀寫均衡或?qū)懖僮黝l繁:優(yōu)先
Collections.synchronizedList()
或手動鎖(避免CopyOnWriteArrayList
的復制開銷)。 - 避免使用
Vector
(性能差,已過時)。
- 讀多寫少:優(yōu)先
總結(jié)
多線程安全修改List
的核心是保證操作的原子性和可見性,常用方案對比:
方案 | 原理 | 優(yōu)點 | 缺點 | 適用場景 |
---|---|---|---|---|
Vector | 同步方法 | 簡單直接 | 性能差,同步粒度粗 | 兼容舊代碼(不推薦新用) |
synchronizedList | 同步塊包裝 | 適配所有List ,實現(xiàn)簡單 | 所有操作加鎖,并發(fā)性能一般 | 讀寫均衡的基礎場景 |
CopyOnWriteArrayList | 寫時復制 | 讀操作無鎖,性能極佳 | 寫操作成本高,數(shù)據(jù)實時性差 | 讀多寫少(如緩存、配置) |
手動鎖(synchronized /Lock ) | 自定義同步粒度 | 靈活控制鎖范圍 | 需手動處理鎖釋放,易出錯 | 復雜場景(如復合操作) |
根據(jù)實際業(yè)務的讀寫頻率和復雜度選擇合適方案即可。
到此這篇關于java中在多線程的情況下安全的修改list(常見解決方案)的文章就介紹到這了,更多相關java多線程修改list內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
SpringBoot配置連接兩個或多個數(shù)據(jù)庫的實現(xiàn)
本文主要介紹了SpringBoot配置連接兩個或多個數(shù)據(jù)庫的實現(xiàn),文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2023-05-05Spring Security Oauth2.0 實現(xiàn)短信驗證碼登錄示例
本篇文章主要介紹了Spring Security Oauth2.0 實現(xiàn)短信驗證碼登錄示例,具有一定的參考價值,感興趣的小伙伴們可以參考一下2018-01-01在Spring Data JPA中引入Querydsl的實現(xiàn)方式
這篇文章主要介紹了在Spring Data JPA中引入Querydsl的實現(xiàn)方式,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2021-01-01Java實現(xiàn)彩色圖片轉(zhuǎn)換為灰度圖片的示例代碼
將彩色圖片轉(zhuǎn)換為灰度圖片是圖像處理中的常見操作,通常用于簡化圖像、增強對比度、或者進行后續(xù)的圖像分析,本項目的目標是通過Java實現(xiàn)將彩色圖片轉(zhuǎn)換為灰度圖片,需要的朋友可以參考下2025-02-02springboot開啟mybatis駝峰命名自動映射的三種方式
這篇文章給大家總結(jié)springboot開啟mybatis駝峰命名自動映射的三種方式,文章并通過代碼示例給大家介紹的非常詳細,具有一定的參考價值,需要的朋友可以參考下2024-02-02