淺析CopyOnWriteArrayList
CopyOnWriteArrayList引入
模擬傳統的ArrayList出現執行緒不安全的現象
public class Demo1 {
public static void main(String[] args) {
//List<String> list = new CopyOnWriteArrayList<>();
List<String> list = new ArrayList<>();
//開啟50個執行緒往ArrayList中添加數據
for (int i = 1; i <= 50; i++) {
new Thread(() -> {
list.add(UUID.randomUUID().toString().substring(0, 5));
System.out.println(list);
}, String.valueOf(i)).start();
}
}
}
運行結果如下:由於fail-fast機制的存在,拋出了modcount修改異常的錯誤(modcount是ArrayList源碼中的一個變數,用來表示修改的次數,因為ArrayList不是為並發情況而設計的集合類)
如何解決該問題呢?
方式一:可以使用Vector集合,Vector集合是執行緒安全版的ArrayList,其方法都上了一層synchronized進行修飾,採取jvm內置鎖來保證其並發情況下的原子性、可見性、有序性。但同時也帶來了性能問題,因為synchronized一旦膨脹到重量級鎖,存在用戶態到和心態的一個轉變,多執行緒的上下文切換會帶來開銷。另一個問題是Vector集合的擴容沒有ArrayList的策略好
List<String> list = new Vector<>();
方式二:使用Collections.synchronizedList
List<String> list = Collections.synchronizedList(new ArrayList<>());
方式三:採用JUC提供的並發容器,CopyOnWriteArrayList
List<String> list = new CopyOnWriteArrayList<>();
CopyOnWriteArrayList淺析
和ArrayList一樣,其底層數據結構也是數組,加上transient不讓其被序列化,加上volatile修飾來保證多執行緒下的其可見性和有序性
先來看看其構造函數是怎麼一回事
public CopyOnWriteArrayList() {
//默認創建一個大小為0的數組
setArray(new Object[0]);
}
final void setArray(Object[] a) {
array = a;
}
public CopyOnWriteArrayList(Collection<? extends E> c) {
Object[] elements;
//如果當前集合是CopyOnWriteArrayList的類型的話,直接賦值給它
if (c.getClass() == CopyOnWriteArrayList.class)
elements = ((CopyOnWriteArrayList<?>)c).getArray();
else {
//否則調用toArra()將其轉為數組
elements = c.toArray();
// c.toArray might (incorrectly) not return Object[] (see 6260652)
if (elements.getClass() != Object[].class)
elements = Arrays.copyOf(elements, elements.length, Object[].class);
}
//設置數組
setArray(elements);
}
public CopyOnWriteArrayList(E[] toCopyIn) {
//將傳進來的數組元素拷貝給當前數組
setArray(Arrays.copyOf(toCopyIn, toCopyIn.length, Object[].class));
}
在來看看其讀數據的幾個操作,可見都沒上鎖,這就奇怪了,那如何去保證執行緒安全呢?
final Object[] getArray() {
return array;
}
public int size() {
return getArray().length;
}
public boolean isEmpty() {
return size() == 0;
}
public int indexOf(E e, int index) {
Object[] elements = getArray();
return indexOf(e, elements, index, elements.length);
}
public int lastIndexOf(Object o) {
Object[] elements = getArray();
return lastIndexOf(o, elements, elements.length - 1);
}
........
在來看看其修改時的add函數
public boolean add(E e) {
//使用ReentrantLock上鎖
final ReentrantLock lock = this.lock;
lock.lock();
try {
//調用getArray()獲取原來的數組
Object[] elements = getArray();
int len = elements.length;
//複製老數組,得到一個長度+1的數組
Object[] newElements = Arrays.copyOf(elements, len + 1);
//添加元素,在用setArray()函數替換原數組
newElements[len] = e;
setArray(newElements);
return true;
} finally {
lock.unlock();
}
}
可見其修改操作是基於fail-safe機制,像我們的String一樣,不在原來的對象上直接進行操作,而是複製一份對其進行修改,另外此處的修改操作是利用Lock鎖進行上鎖的,所以保證了執行緒安全問題。
在來看看remove操作,看是不是如此做的
public boolean remove(Object o) {
Object[] snapshot = getArray();
int index = indexOf(o, snapshot, 0, snapshot.length);
return (index < 0) ? false : remove(o, snapshot, index);
}
private boolean remove(Object o, Object[] snapshot, int index) {
final ReentrantLock lock = this.lock;
//上鎖
lock.lock();
try {
Object[] current = getArray();
int len = current.length;
if (snapshot != current) findIndex: {
int prefix = Math.min(index, len);
for (int i = 0; i < prefix; i++) {
if (current[i] != snapshot[i] && eq(o, current[i])) {
index = i;
break findIndex;
}
}
if (index >= len)
return false;
if (current[index] == o)
break findIndex;
index = indexOf(o, current, index, len);
if (index < 0)
return false;
}
//複製一個數組
Object[] newElements = new Object[len - 1];
System.arraycopy(current, 0, newElements, 0, index);
System.arraycopy(current, index + 1,
newElements, index,
len - index - 1);
//替換原數組
setArray(newElements);
return true;
} finally {
lock.unlock();
}
}
可見其思路是一致的,我們在與ArrayList去對比一下,可見其效率比ArrayList低不少,畢竟多執行緒場景下,其每次都是要在原數組基礎上複製一份在操作耗記憶體和時間,而ArrayList只是容量滿了進行擴容,因此在非多執行緒的場景下還是用ArrayList吧。
這也解決了我之前的疑問,為啥還學ArrayList呢,JUC版的CopyOnWriteArrayList可以干ArrayList幹不了的事,咱們直接用CopyOnWriteArrayList不也挺香。
小結
- CopyOnWriteArrayList適合於多執行緒場景下使用,其採用讀寫分離的思想,讀操作不上鎖,寫操作上鎖,且寫操作效率較低
- CopyOnWriteArrayList基於fail-safe機制,每次修改都會在原先基礎上複製一份,修改完畢後在進行替換
- CopyOnWriteArrayList採用的是ReentrantLock進行上鎖。