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ā)讀,讀操作是無鎖的,性能較高。至于寫操作,比如向容器中添加一個(gè)元素,則首先將當(dāng)前容器復(fù)制一份,然后在新副本上執(zhí)行寫操作,結(jié)束之后再將原容器的引用指向新容器。
- 線程安全的,多線程環(huán)境下可以直接使用,無需加鎖;
- 通過鎖 + 數(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需任何同步措施,比較適用于讀多寫少的并發(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;
二是無法保證實(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);
}直接讀取即可,無需加鎖
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)文章
Spring Schedule Task動(dòng)態(tài)改寫Cron配置方式
這篇文章主要介紹了Spring Schedule Task動(dòng)態(tài)改寫Cron配置方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-11-11
maven profile自動(dòng)切換環(huán)境參數(shù)的2種方法詳解
這篇文章主要給大家介紹了關(guān)于maven profile自動(dòng)切換環(huán)境參數(shù)的2種方法,文中通過示例代碼將這兩種方法介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧。2018-04-04
分布式面試消息隊(duì)列解決消息重復(fù)保證消息順序
這篇文章主要介紹了分布式面試問題:分布式消息隊(duì)列如何解決消息重復(fù)并保證消息順序面試問題解答,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步2022-03-03
詳解Spring Cloud Zuul中路由配置細(xì)節(jié)
本篇文章主要介紹了詳解Spring Cloud Zuul中路由配置細(xì)節(jié),小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2017-10-10

