一、寫在開頭 我們在學(xué)習(xí)集合或者說容器的時(shí)候了解到,很多集合并非線程安全的,在并發(fā)場景下,為了保障數(shù)據(jù)的安全性,誕生了并發(fā)容器,廣為人知的有ConcurrentHashMap、ConcurrentLinkedQueue、BlockingQueue等,那你們知道ArrayList也有自己對應(yīng)的并發(fā)容器
我們在學(xué)習(xí)集合或者說容器的時(shí)候了解到,很多集合并非線程安全的,在并發(fā)場景下,為了保障數(shù)據(jù)的安全性,誕生了并發(fā)容器,廣為人知的有ConcurrentHashMap、ConcurrentLinkedQueue、BlockingQueue等,那你們知道ArrayList也有自己對應(yīng)的并發(fā)容器嘛?
作為使用頻率最高的集合類之一,ArrayList線程不安全,我們在并發(fā)環(huán)境下使用,一般要輔以手動上鎖、或者通過Collections.synchronizedList()轉(zhuǎn)一手,為了解決這一問題,Doug Lea(道格.利)大師為我們提供了它的并發(fā)類——
CopyOnWriteArrayList
。
CopyOnWriteArrayList 是java.util.concurrent的并發(fā)類,線程安全,遵循寫時(shí)復(fù)制的原則(CopyOnWrite),什么意思呢?就是我們在對列表進(jìn)行增刪改時(shí),會先創(chuàng)建一個列表的副本,在副本中完成增刪改操作后,再將副本替換原列表,整個過程舊的列表并沒有鎖定,因此原來的讀取操作仍可繼續(xù)。
看到這里細(xì)心的同學(xué)應(yīng)該已經(jīng)發(fā)現(xiàn)了它的“弊端”了,先賦值副本,寫完再替換,這是有時(shí)間差的,沒錯,這就是CopyOnWrite的延時(shí)更新策略,我們在發(fā)生寫的同時(shí),不阻塞讀,但讀取的只是舊列表中的數(shù)據(jù),直到引用替換完成,可以保證 數(shù)據(jù)的最終一致性 ,無法保證實(shí)時(shí)性。
我們來看一下CopyOnWriteArrayList底層的源碼實(shí)現(xiàn),首先在內(nèi)部維護(hù)了一個數(shù)組,用volatile關(guān)鍵字修飾,保證了數(shù)據(jù)的內(nèi)存可見性
/** The array, accessed only via getArray/setArray. */
private transient volatile Object[] array;
讀。篻et()方法
public E get(int index) {
return get(getArray(), index);
}
/**
* Gets the array. Non-private so as to also be accessible
* from CopyOnWriteArraySet class.
*/
final Object[] getArray() {
return array;
}
private E get(Object[] a, int index) {
return (E) a[index];
}
這段源碼沒什么,很好理解,就是普通的讀取數(shù)組的操作,這也能看出CopyOnWriteArrayList的讀是不阻塞的。
新增:add()方法
public boolean add(E e) {
final ReentrantLock lock = this.lock;
//1. 使用Lock,保證寫線程在同一時(shí)刻只有一個
lock.lock();
try {
//2. 獲取舊數(shù)組引用
Object[] elements = getArray();
int len = elements.length;
//3. 創(chuàng)建新的數(shù)組,并將舊數(shù)組的數(shù)據(jù)復(fù)制到新數(shù)組中
Object[] newElements = Arrays.copyOf(elements, len + 1);
//4. 往新數(shù)組中添加新的數(shù)據(jù)
newElements[len] = e;
//5. 將舊數(shù)組引用指向新的數(shù)組
setArray(newElements);
return true;
} finally {
lock.unlock();
}
}
final void setArray(Object[] a) {
array = a;
}
通過這段源碼,我們就能夠感知到前面描述的實(shí)現(xiàn)原理了, 首先 ,新增元素時(shí),內(nèi)部通過可重入鎖進(jìn)行鎖定,說明寫時(shí)會獨(dú)占, 然后 ,再將原數(shù)組賦值到一個新數(shù)組中, 最后 ,將舊數(shù)組的引用指向新數(shù)組。
對于CopyOnWriteArrayList的日常使用,和ArrayList幾乎一模一樣,在這里就不用過多介紹了,但它的使用還是需要注意的,雖然可以保證線程安全,但因其特性所致,僅適應(yīng)于讀多寫少的并發(fā)環(huán)境,對于頻繁寫入或者寫入的對象較大,一定不要使用CopyOnWriteArrayList容器,不然會坑死你的!
【舉個例子】
之前在這篇文章中:[EasyExcel導(dǎo)入導(dǎo)出百萬數(shù)據(jù)量]
采用了CopyOnWriteArrayList,以此來保證在多線程寫入數(shù)據(jù)庫時(shí)的線程安全,由于寫入的excel文件中有100萬的數(shù)據(jù)量,再導(dǎo)入的時(shí)候非常之慢,用了514秒!
核心實(shí)現(xiàn)代碼如下,具體內(nèi)容實(shí)現(xiàn)可去看那篇文章哈
@Slf4j
@Service
public class EasyExcelImportHandler implements ReadListener {
/*成功數(shù)據(jù)*/
private final CopyOnWriteArrayList successList = new CopyOnWriteArrayList<>();
/*單次處理?xiàng)l數(shù)*/
private final static int BATCH_COUNT = 20000;
@Resource
private ThreadPoolExecutor threadPoolExecutor;
@Resource
private UserMapper userMapper;
@Override
public void invoke(User user, AnalysisContext analysisContext) {
if(StringUtils.isNotBlank(user.getName())){
successList.add(user);
return;
}
if(successList.size() >= BATCH_COUNT){
log.info("讀取數(shù)據(jù):{}", successList.size());
saveData();
}
}
/**
* 采用多線程讀取數(shù)據(jù)
*/
private void saveData() {
List> lists = ListUtil.split(successList, 20000);
CountDownLatch countDownLatch = new CountDownLatch(lists.size());
for (List list : lists) {
threadPoolExecutor.execute(()->{
try {
userMapper.insertSelective(list.stream().map(o -> {
User user = new User();
user.setName(o.getName());
user.setId(o.getId());
user.setPhoneNum(o.getPhoneNum());
user.setAddress(o.getAddress());
return user;
}).collect(Collectors.toList()));
} catch (Exception e) {
log.error("啟動線程失敗,e:{}", e.getMessage(), e);
} finally {
//執(zhí)行完一個線程減1,直到執(zhí)行完
countDownLatch.countDown();
}
});
}
// 等待所有線程執(zhí)行完
try {
countDownLatch.await();
} catch (Exception e) {
log.error("等待所有線程執(zhí)行完異常,e:{}", e.getMessage(), e);
}
// 提前將不再使用的集合清空,釋放資源
successList.clear();
lists.clear();
}
/**
* 所有數(shù)據(jù)讀取完成之后調(diào)用
* @param analysisContext
*/
@Override
public void doAfterAllAnalysed(AnalysisContext analysisContext) {
//讀取剩余數(shù)據(jù)
if(CollectionUtils.isNotEmpty(successList)){
log.info("讀取數(shù)據(jù):{}條",successList.size());
saveData();
}
}
}
而將這段代碼中的CopyOnWriteArrayList換為ArrayList。
/*成功數(shù)據(jù)*/
// private final CopyOnWriteArrayList successList = new CopyOnWriteArrayList<>();
private final List successList = new ArrayList<>();
導(dǎo)入100萬數(shù)據(jù)量的耗時(shí),直接從分鐘降為秒級,由此可見CopyOnWriteArrayList在寫入大對象時(shí)的性能非常之差!
通過以上的學(xué)習(xí),我們進(jìn)行總結(jié):CopyOnWriteArrayList的優(yōu)勢在于可以保證線程安全的同時(shí),不阻塞讀操作,但是這僅限于讀多寫少的情況;
在寫多讀少的情況下,或者寫入的對象占用內(nèi)容較大時(shí),不建議使用CopyOnWriteArrayList;CopyOnWrite 容器只能保證數(shù)據(jù)的最終一致性,不能保證數(shù)據(jù)的實(shí)時(shí)一致性。所以如果你希望寫入的的數(shù)據(jù),馬上能讀到,請不要使用 CopyOnWrite 容器,最好通過 ReentrantReadWriteLock 自定義一個的列表。
如果本篇博客對您有一定的幫助,大家記得 留言+點(diǎn)贊+收藏 呀。原創(chuàng)不易,轉(zhuǎn)載請聯(lián)系Build哥!
機(jī)器學(xué)習(xí):神經(jīng)網(wǎng)絡(luò)構(gòu)建(下)
閱讀華為Mate品牌盛典:HarmonyOS NEXT加持下游戲性能得到充分釋放
閱讀實(shí)現(xiàn)對象集合與DataTable的相互轉(zhuǎn)換
閱讀鴻蒙NEXT元服務(wù):論如何免費(fèi)快速上架作品
閱讀算法與數(shù)據(jù)結(jié)構(gòu) 1 - 模擬
閱讀5. Spring Cloud OpenFeign 聲明式 WebService 客戶端的超詳細(xì)使用
閱讀Java代理模式:靜態(tài)代理和動態(tài)代理的對比分析
閱讀Win11筆記本“自動管理應(yīng)用的顏色”顯示規(guī)則
閱讀本站所有軟件,都由網(wǎng)友上傳,如有侵犯你的版權(quán),請發(fā)郵件[email protected]
湘ICP備2022002427號-10 湘公網(wǎng)安備:43070202000427號© 2013~2025 haote.com 好特網(wǎng)