Java中的CopyOnWriteArrayList容器解析
1. 簡(jiǎn)介
在 ArrayList 的類注釋上,JDK 就提醒了我們,如果要把 ArrayList 作為共享變量的話,是線程不安全的,推薦我們自己加鎖或者使用 Collections.synchronizedList 方法,其實(shí) JDK 還提供了另外一種線程安全的 List,叫做 CopyOnWriteArrayList
2. 原理
很多時(shí)候,我們的系統(tǒng)應(yīng)對(duì)的都是讀多寫少的并發(fā)場(chǎng)景。CopyOnWriteArrayList容器允許并發(fā)讀,讀操作是無(wú)鎖的,性能較高。至于寫操作,比如向容器中添加一個(gè)元素,則首先將當(dāng)前容器復(fù)制一份,然后在新副本上執(zhí)行寫操作,結(jié)束之后再將原容器的引用指向新容器。
- 線程安全的,多線程環(huán)境下可以直接使用,無(wú)需加鎖;
- 通過鎖 + 數(shù)組拷貝 + volatile 關(guān)鍵字保證了線程安全;
- 每次數(shù)組操作,都會(huì)把數(shù)組拷貝一份出來,在新數(shù)組上進(jìn)行操作,操作成功之后再賦值回去。

從整體架構(gòu)上來說,CopyOnWriteArrayList 數(shù)據(jù)結(jié)構(gòu)和 ArrayList 是一致的,底層是個(gè)數(shù)組,只不過 CopyOnWriteArrayList 在對(duì)數(shù)組進(jìn)行操作的時(shí)候,基本會(huì)分四步走:
- 加鎖;
- 從原數(shù)組中拷貝出新數(shù)組;
- 在新數(shù)組上進(jìn)行操作,并把新數(shù)組賦值給數(shù)組容器;
- 解鎖
除了加鎖之外,CopyOnWriteArrayList 的底層數(shù)組還被 volatile 關(guān)鍵字修飾,意思是一旦數(shù)組被修改,其它線程立馬能夠感知到,代碼如下:
private transient volatile Object[] array;
整體上來說,CopyOnWriteArrayList 就是利用鎖 + 數(shù)組拷貝 + volatile 關(guān)鍵字保證了 List 的線程安全。
3. 優(yōu)點(diǎn)
讀操作(不加鎖)性能很高,因?yàn)闊o(wú)需任何同步措施,比較適用于讀多寫少的并發(fā)場(chǎng)景。Java的list在遍歷時(shí),若中途有別的線程對(duì)list容器進(jìn)行修改,則會(huì)拋ConcurrentModificationException異常。而CopyOnWriteArrayList由于其"讀寫分離"的思想,遍歷和修改操作分別作用在不同的list容器,所以在使用迭代器進(jìn)行遍歷時(shí)候,也就不會(huì)拋出ConcurrentModificationException異常了。
4. 缺點(diǎn)
一是內(nèi)存占用問題,畢竟每次執(zhí)行寫操作都要將原容器拷貝一份。數(shù)據(jù)量大時(shí),對(duì)內(nèi)存壓力較大,可能會(huì)引起頻繁GC;
二是無(wú)法保證實(shí)時(shí)性,因?yàn)镃opyOnWrite的寫時(shí)復(fù)制機(jī)制,所以在進(jìn)行寫操作的時(shí)候,內(nèi)存里會(huì)同時(shí)駐扎兩個(gè)對(duì)象的內(nèi)存,舊的對(duì)象和新寫入的對(duì)象(注意:在復(fù)制的時(shí)候只是復(fù)制容器里的引用,只是在寫的時(shí)候會(huì)創(chuàng)建新對(duì)象添加到新容器里,而舊容器的對(duì)象還在使用,所以有兩份對(duì)象內(nèi)存)。
5. 源碼分析
5.1 添加操作
public boolean add(E e) {
//ReentrantLock加鎖,保證線程安全
final ReentrantLock lock = this.lock;
lock.lock();
try {
Object[] elements = getArray();
int len = elements.length;
//拷貝原容器,長(zhǎng)度為原容器長(zhǎng)度加一
Object[] newElements = Arrays.copyOf(elements, len + 1);
//在新副本上執(zhí)行添加操作
newElements[len] = e;
//將原容器引用指向新副本
setArray(newElements);
return true;
} finally {
//解鎖
lock.unlock();
}
}添加的邏輯很簡(jiǎn)單,先將原容器copy一份,然后在新副本上執(zhí)行寫操作,之后再切換引用。當(dāng)然此過程是要加鎖的。
5.2 刪除操作
public E remove(int index) {
//加鎖
final ReentrantLock lock = this.lock;
lock.lock();
try {
Object[] elements = getArray();
int len = elements.length;
E oldValue = get(elements, index);
int numMoved = len - index - 1;
if (numMoved == 0)
//如果要?jiǎng)h除的是列表末端數(shù)據(jù),拷貝前l(fā)en-1個(gè)數(shù)據(jù)到新副本上,再切換引用
setArray(Arrays.copyOf(elements, len - 1));
else {
//否則,將除要?jiǎng)h除元素之外的其他元素拷貝到新副本中,并切換引用
Object[] newElements = new Object[len - 1];
System.arraycopy(elements, 0, newElements, 0, index);
System.arraycopy(elements, index + 1, newElements, index,
numMoved);
setArray(newElements);
}
return oldValue;
} finally {
//解鎖
lock.unlock();
}
}刪除操作同理,將除要?jiǎng)h除元素之外的其他元素拷貝到新副本中,然后切換引用,將原容器引用指向新副本。同屬寫操作,需要加鎖。
我們?cè)賮砜纯醋x操作,CopyOnWriteArrayList的讀操作是不用加鎖的,性能很高。
public E get(int index) {
return get(getArray(), index);
}直接讀取即可,無(wú)需加鎖
private E get(Object[] a, int index) {
return (E) a[index];
}5.3 弱一致性的迭代器
所謂弱一致性是指返回迭代器后,其他線程對(duì)list的增刪改查對(duì)迭代器是不可見的
// 演示多線程下迭代器的弱一致性結(jié)果
public class copylist {
private static volatile CopyOnWriteArrayList<String> arrayList = new CopyOnWriteArrayList<>();
public static void main(String[] args) throws InterruptedException {
arrayList.add("hello");
arrayList.add("alibaba");
arrayList.add("welcome");
arrayList.add("to");
arrayList.add("hangzhou");
Thread threadOne = new Thread(new Runnable() {
@Override
public void run() {
// 修改list中下標(biāo)為1的元素為ali
arrayList.set(1, "ali");
// 刪除元素
arrayList.remove(2);
arrayList.remove(3);
}
});
// 保證在修改線程啟動(dòng)前獲取迭代器
Iterator<String> itr = arrayList.iterator();
// 啟動(dòng)線程
threadOne.start();
// 等待子線程執(zhí)行完畢
threadOne.join();
while(itr.hasNext()) {
System.out.println(itr.next());
}
}
}執(zhí)行程序:
hello
alibaba
welcome
to
hangzhou
Process finished with exit code 0
從輸出結(jié)果我們知道,在子線程里面進(jìn)行的操作一個(gè)都沒有生效,這就是迭代器弱一致性的體現(xiàn)。需要注意的是,獲取迭代器的操作必須在子線程操作之前進(jìn)行。
6. ArrayList轉(zhuǎn)為線程安全的方法
List list = Collections.synchronizedList(new ArrayList());
到此這篇關(guān)于Java中的CopyOnWriteArrayList容器解析的文章就介紹到這了,更多相關(guān)CopyOnWriteArrayList解析內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
SpringBoot?整合?Quartz?定時(shí)任務(wù)框架詳解
這篇文章主要介紹了SpringBoot整合Quartz定時(shí)任務(wù)框架詳解,Quartz是一個(gè)完全由Java編寫的開源作業(yè)調(diào)度框架,為在Java應(yīng)用程序中進(jìn)行作業(yè)調(diào)度提供了簡(jiǎn)單卻強(qiáng)大的機(jī)制2022-08-08
Eclipse運(yùn)行android項(xiàng)目報(bào)錯(cuò)Unable to build: the file dx.jar was not
今天小編就為大家分享一篇關(guān)于Eclipse運(yùn)行android項(xiàng)目報(bào)錯(cuò)Unable to build: the file dx.jar was not loaded from the SDK folder的解決辦法,小編覺得內(nèi)容挺不錯(cuò)的,現(xiàn)在分享給大家,具有很好的參考價(jià)值,需要的朋友一起跟隨小編來看看吧2018-12-12
10個(gè)SpringBoot框架內(nèi)置的實(shí)用功能詳解
在 Spring Boot 開發(fā)中,框架內(nèi)置的諸多實(shí)用功能猶如一把把利刃,能讓開發(fā)者在項(xiàng)目的各個(gè)階段都事半功倍,這些功能無(wú)需額外集成,通過簡(jiǎn)單配置或編碼即可快速實(shí)現(xiàn)常見需求,下面將為你深入解析一系列極具價(jià)值的內(nèi)置功能,需要的朋友可以參考下2025-06-06
SpringBoot數(shù)據(jù)庫(kù)初始化datasource配置方式
這篇文章主要為大家介紹了SpringBoot數(shù)據(jù)庫(kù)初始化datasource配置方式,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-12-12
深入了解Java設(shè)計(jì)模式之職責(zé)鏈模式
Java設(shè)計(jì)模式中有很多種類別,例如單例模式、裝飾模式、觀察者模式等。本文將為大家詳細(xì)介紹其中的職責(zé)鏈模式,感興趣的可以了解一下2022-09-09
Lombok中@Builder和@SuperBuilder注解的用法案例
@Builder?是?lombok?中的注解,可以使用builder()構(gòu)造的Person.PersonBuilder對(duì)象進(jìn)行鏈?zhǔn)秸{(diào)用,給所有屬性依次賦值,這篇文章主要介紹了Lombok中@Builder和@SuperBuilder注解的用法,需要的朋友可以參考下2023-01-01
SpringBoot導(dǎo)出Excel表格到指定路徑的代碼詳解
Spring Boot導(dǎo)出Excel通常涉及到使用第三方庫(kù)如Apache POI或者XlsxWriter等,它們能幫助你在Spring應(yīng)用中生成并下載Excel文件,那么SpringBoot如何導(dǎo)出Excel表格到指定路徑,本文將給大家詳細(xì)的介紹一下2024-07-07

