CopyOnWriteArrayList是開發(fā)過程中常用的一種并發(fā)容器,多用于讀多寫少的并發(fā)場景。但是CopyOnWriteArrayList真的能做到完全的線程安全嗎?
答案是并不能。
CopyOnWriteArrayList原理
我們可以看出當我們向容器添加或刪除元素的時候虎敦,不直接往當前容器添加刪除,而是先將當前容器進行Copy政敢,復(fù)制出一個新的容器其徙,然后新的容器里添加刪除元素,添加刪除完元素之后堕仔,再將原容器的引用指向新的容器擂橘,整個過程加鎖,保證了寫的線程安全摩骨。
public boolean add(E e) {
synchronized (lock) {
Object[] elements = getArray();
int len = elements.length;
Object[] newElements = Arrays.copyOf(elements, len + 1);
newElements[len] = e;
setArray(newElements);
return true;
}
}
public E remove(int index) {
synchronized (lock) {
Object[] elements = getArray();
int len = elements.length;
E oldValue = get(elements, index);
int numMoved = len - index - 1;
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);
setArray(newElements);
}
return oldValue;
}
}
而因為寫操作的時候不會對當前容器做任何處理,所以我們可以對容器進行并發(fā)的讀朗若,而不需要加鎖恼五,也就是讀寫分離。
public E get(int index) {
return get(getArray(), index);
}
一般來講我們使用時哭懈,會用一個線程向容器中添加元素灾馒,一個線程來讀取元素,而讀取的操作往往更加頻繁遣总。寫操作加鎖保證了線程安全睬罗,讀寫分離保證了讀操作的效率,簡直完美旭斥。
數(shù)組越界
但想象一下如果這時候有第三個線程進行刪除元素操作容达,讀線程去讀取容器中最后一個元素,讀之前的時候容器大小為i垂券,當去讀的時候刪除線程突然刪除了一個元素花盐,這個時候容器大小變?yōu)榱薸-1,讀線程仍然去讀取第i個元素,這時候就會發(fā)生數(shù)組越界算芯。
測試一下柒昏,首先向CopyOnWriteArrayList里面塞10000個測試數(shù)據(jù),啟動兩個線程熙揍,一個不斷的刪除元素职祷,一個不斷的讀取容器中最后一個數(shù)據(jù)。
public void test(){
for(int i = 0; i<10000; i++){
list.add("string" + i);
}
new Thread(new Runnable() {
@Override
public void run() {
while (true) {
if (list.size() > 0) {
String content = list.get(list.size() - 1);
}else {
break;
}
}
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
while (true) {
if(list.size() <= 0){
break;
}
list.remove(0);
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}).start();
}
運行届囚,可以看出刪除到第7個元素的時候就發(fā)生了數(shù)組越界
從上可以看出CopyOnWriteArrayList并不是完全意義上的線程安全堪旧,如果涉及到remove操作,一定要謹慎處理奖亚。