今天來(lái)復(fù)習(xí)一下集合智绸。在支持并發(fā)的集合中达吞,我覺得CopyOnWriteArrayList是相對(duì)容易理解的一個(gè)昵观。
CopyOnWrite:寫時(shí)復(fù)制蜂奸,就是當(dāng)有線程向集合添加元素時(shí),不是直接往舊的容器中添加元素嚷节,而是將舊的容器中的元素復(fù)制到新的容器中聂儒,讀的時(shí)候讀的仍然是舊容器,這樣就不會(huì)影響并發(fā)讀了
分析一下CopyOnWriteArrayList部分源碼
1.add(E e)方法丹喻,向容器中添加元素
public boolean add(E e) {
final ReentrantLock lock = this.lock;
lock.lock();
try {
Object[] elements = getArray();
int len = elements.length;
Object[] newElements = Arrays.copyOf(elements, len + 1);
newElements[len] = e;
setArray(newElements);
return true;
} finally {
lock.unlock();
}
}
在往容器中加元素的過程是加鎖的,加鎖是通過可重入鎖ReentrantLock實(shí)現(xiàn)的翁都,如果不加鎖的話碍论,多個(gè)線程同時(shí)添加元素會(huì)復(fù)制多次。
getArray()獲得舊容器中的元素
final Object[] getArray() {
return array;
}
Arrays.copyOf(elements, len + 1)進(jìn)行數(shù)組的復(fù)制柄慰,并返回復(fù)制以后新的數(shù)組
newElements[len] = e向新的集合中添加元素
setArray(newElements)將舊容器的引用指向新容器
final void setArray(Object[] a) {
array = a;
}
其余向容器中添加元素的方法鳍悠,比如public void add(int index, E element)
實(shí)現(xiàn)思路和add(E e)大抵相同
2.get(int index),從容器中獲得指定位置的元素
public E get(int index) {
return get(getArray(), index);
}
實(shí)際調(diào)用的是get(Object[] a, int index)方法
private E get(Object[] a, int index) {
return (E) a[index];
}
得到指定位置的元素就是獲得數(shù)組中指定位置的元素
從代碼中可以看到,對(duì)于從容器中讀操作是不進(jìn)行加鎖的
3.remove(int index)坐搔,容器中移除元素
public E remove(int index) {
final ReentrantLock lock = this.lock;
lock.lock();
try {
//獲得原來(lái)舊的容器
Object[] elements = getArray();
int len = elements.length;
//獲得指定位置上的元素
E oldValue = get(elements, index);
//移動(dòng)的距離
int numMoved = len - index - 1;
//如果集合中只有一個(gè)元素
if (numMoved == 0)
setArray(Arrays.copyOf(elements, len - 1));
else {
Object[] newElements = new Object[len - 1];
System.arraycopy(elements, 0, newElements, 0, index);
System.arraycopy(elements, index + 1, newElements, index,
numMoved);
//將原來(lái)的舊容器的引用指向新的引用
setArray(newElements);
}
return oldValue;
} finally {
lock.unlock();
}
}
實(shí)現(xiàn)的基本思想和add(E e)相同藏研,需要進(jìn)行加鎖,并且會(huì)對(duì)舊的容器進(jìn)行復(fù)制
4.看一個(gè)CopyOnWriteArrayList的構(gòu)造函數(shù)
public CopyOnWriteArrayList() {
setArray(new Object[0]);
}
一般我們自己寫項(xiàng)目的時(shí)候都會(huì)選擇使用這個(gè)構(gòu)造函數(shù)概行。這個(gè)構(gòu)造函數(shù)并沒有初始化集合的大小蠢挡,集合大小為0,但是它沒有像ArrayList一樣有擴(kuò)容操作凳忙,因?yàn)樵谕@個(gè)容器中進(jìn)行寫操作時(shí)业踏,實(shí)際上并不是往當(dāng)前容器添加元素,而是會(huì)創(chuàng)建出一個(gè)比當(dāng)前容器大1的容器涧卵,在對(duì)往這個(gè)容器中添加元素勤家。所以不需要進(jìn)行擴(kuò)容×郑或者可以這么理解伐脖,它在每次添加元素的操作時(shí)都進(jìn)行了一次擴(kuò)容,每次擴(kuò)容一個(gè)元素的大小
5.CopyOnWriteArrayList的缺點(diǎn):
最明顯的一個(gè)致命缺點(diǎn)就是占大量的內(nèi)存乐设,在往容器中刪除元素和添加元素的時(shí)候都會(huì)在創(chuàng)建一個(gè)新的數(shù)組讼庇,如果垃圾收集器回收不及時(shí)的話,并且有很多線程進(jìn)行寫操作近尚,可能會(huì)撐爆內(nèi)存吧巫俺。
在一篇博客上看到CopyOnWrite容器只能保證數(shù)據(jù)的最終一致性,不能保證數(shù)據(jù)的實(shí)時(shí)一致性肿男。所以如果你希望寫入的的數(shù)據(jù)介汹,馬上能讀到却嗡,請(qǐng)不要使用CopyOnWrite容器。不是特別理解這一點(diǎn)嘹承,volatile Object[] array窗价,array是有volatile修飾的,其保證了內(nèi)存的可見性叹卷,當(dāng)一個(gè)線程對(duì)一個(gè)共享變量的寫操作時(shí)撼港,寫完立刻就會(huì)對(duì)其他線程立即可見,那只要寫完骤竹,其他線程就能讀到新添加的值帝牡。自己寫了個(gè)demo進(jìn)行測(cè)試,感覺延遲效果不是很明顯
測(cè)試類:TestCopyOnWriteArrayList.java
public class TestCopyOnWriteArrayList {
private static CopyOnWriteArrayList<String> c = new CopyOnWriteArrayList<>();
private static long startTime;
private static long endTime;
public static void main(String[] args) {
new Thread(new Runnable() {
@Override
public void run() {
c.add("a");
startTime = System.currentTimeMillis(); //獲得添加a以后的時(shí)間
System.out.println("添加了a");
//添加了a以后讓其睡眠蒙揣,讓其他線程有時(shí)間執(zhí)行
try {
Thread.currentThread().sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
c.add("b");
System.out.println("添加了b");
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.currentThread().sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
c.add("c");
System.out.println("添加了c");
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.currentThread().sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
c.add("d");
System.out.println("添加了d");
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
// System.out.println("讀取第一個(gè)元素");
String s = c.get(0);
endTime = System.currentTimeMillis();
System.out.println("讀取到a花費(fèi)時(shí)間:" + (endTime - startTime) + "毫秒");
System.out.println("s: " + s);
}
}).start();
}
}
運(yùn)行結(jié)果:
添加了a
讀取到a花費(fèi)時(shí)間:1毫秒
s: a
添加了b
添加了c
添加了d
1毫秒的延遲也不是特別長(zhǎng)吧
不知道是不是自己的例子不正確
6.CopyOnWriteArrayList的應(yīng)用場(chǎng)景:讀多寫少的場(chǎng)景