CopyOnWriteArrayList是一個線程安全的ArrayList,對其進行的修改操作都是在底層的一個複製的數組(快照)上進行的,也就是使用了寫時複製策略。如圖所示是CopyOnWriteArrayList的類圖結構:
上圖有個小瑕疵,lock 是 包級私有,而不是 protected。
能夠看到,每個CopyOnWriteArrayList對象都有一個array數組用來存放具體元素,而ReenTrantLock則用來保證只有一個線程對Array進行修改。ReenTrantLock本身是一個獨佔鎖,同時只有一個線程能夠獲取。接下來看一下其中的一些方法代碼。
初始化
共有三個構造函數:
public CopyOnWriteArrayList() {
setArray(new Object[0]); //創建一個大小爲0的Object數組作爲array初始值
}
public CopyOnWriteArrayList(E[] toCopyIn) {
//創建一個list,其內部元素是toCopyIn的的副本
setArray(Arrays.copyOf(toCopyIn, toCopyIn.length, Object[].class));
}
//將傳入參數集合中的元素複製到本list中
public CopyOnWriteArrayList(Collection<? extends E> c) {
Object[] elements;
if (c.getClass() == CopyOnWriteArrayList.class)
elements = ((CopyOnWriteArrayList<?>)c).getArray();
else {
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);
}
setArray方法很簡單:
final void setArray(Object[] a) {
array = a;
}
添加元素
添加元素有很多方法,包括add(E e), add(int index, E element)等,原理基本上相同,所以我們只看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();
}
}
代碼很簡單,就是將原來的元素複製到了一個新數組中,且長度應該加1,然後在新數組末尾加上要添加的元素,最後設置新數組爲自己的array。
獲取指定位置元素
使用E get(int index)方法獲取下標爲index的元素:
public E get(int index) {
return get(getArray(), index);
}
final Object[] getArray() {
return array;
}
private E get(Object[] a, int index) {
return (E) a[index];
}
這個方法是線程不安全的,因爲這個分成了兩步,分別是獲取數組和獲取元素,而且中間過程沒有加鎖。假設當前線程在獲取數組(執行getArray())後,其他線程修改了這個CopyOnWriteArrayList,那麼它裏面的元素就會改變,但此時當前線程返回的仍然是舊的數組,所以返回的元素就不是最新的了,這就是寫時複製策略產生的弱一致性問題。
修改指定元素
使用E set (int index, E element)修改list中指定元素的值,代碼如下:
public E set(int index, E element) {
final ReentrantLock lock = this.lock;
lock.lock(); //加鎖
try {
Object[] elements = getArray();
E oldValue = get(elements, index); //先得到要修改的舊值
if (oldValue != element) { //值確實修改了
int len = elements.length;
//將array複製到新數組,並進行修改,並設置array爲新數組
Object[] newElements = Arrays.copyOf(elements, len);
newElements[index] = element;
setArray(newElements);
} else {
// 雖然值確實沒改,但要保證volatile語義,需重新設置array
setArray(elements);
}
return oldValue;
} finally {
lock.unlock();
}
}
刪除元素
使用public E remove(int index)方法,代碼如下:
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)
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;
} finally {
lock.unlock();
}
}
也很簡單,就是將元素分兩次複製到新數組中,然後設置array爲新數組。返回的是刪除的元素。
弱一致性的迭代器
我們先看一下迭代器是怎麼使用的:
public static void main(String[] args) {
CopyOnWriteArrayList<String> arrayList = new CopyOnWriteArrayList<>();
arrayList.add("hello");
arrayList.add("alibaba");
Iterator<String> itr = arrayList.iterator();
while (((Iterator) itr).hasNext())
System.out.println(itr.next());
}
很簡單,那弱一致性是怎麼回事呢,它是指返回迭代器後,其他線程對list的增刪改對迭代器是不可見的。接下來看一下爲什麼會這樣:
public Iterator<E> iterator() {
return new COWIterator<E>(getArray(), 0); //返回一個COWIterator對象
}
static final class COWIterator<E> implements ListIterator<E> {
/** 數組array快照 */
private final Object[] snapshot;
/** 數組下標 */
private int cursor;
private COWIterator(Object[] elements, int initialCursor) {
cursor = initialCursor;
snapshot = elements;
}
public boolean hasNext() {
return cursor < snapshot.length;
}
@SuppressWarnings("unchecked")
public E next() {
if (! hasNext())
throw new NoSuchElementException();
return (E) snapshot[cursor++];
}
在調用iterator()方法後,會返回一個COWIterator對象,COWIterator對象的snapshot變量保存了當前list的內容,cursor是遍歷list時數據的下標。
那麼爲什麼說snapshot是list的快找呢,明明傳的是引用。其實這就和CopyOnWriteArrayList本身有關了,如果在返回迭代器後沒有對裏面的數組array進行修改,則這兩個變量指向的確實是同一個數組;但是若修改了,則根據前面所講,它是會新建一個數組,然後將修改後的數組複製到新建的數組,而老的數組就會被“丟棄”,所以如果修改了數組,則此時snapshot指向的還是原來的數組,而array變量已經指向了新的修改後的數組了。這也就說明獲取迭代器後,使用迭代器元素時,其他線程對該list的增刪改不可見,因爲他們操作的是兩個不同的數組,這就是弱一致性。
接下來就演示一下這個現象:
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() {
arrayList.set(1, "baba");
arrayList.remove(2);
arrayList.remove(3);
}
});
Iterator<String> itr = arrayList.iterator();
threadOne.start();
threadOne.join();
while (itr.hasNext())
System.out.println(itr.next());
}
}
運行結果如下,說明雖然線程threadOne改變了這個list,但是獲取了迭代器後,它指向的還是舊的數組,所以遍歷的時候還是舊的數組內容。所以==獲取迭代器的操作必須在子線程操作之前進行。
hello
alibaba
welcome
to
hangzhou
總結
CopyOnWriteArrayList使用寫時複製策略保證list的一致性,而獲取–修改–寫入三個步驟不是原子性,所以需要一個獨佔鎖保證修改數據時只有一個線程能夠進行。另外,CopyOnWriteArrayList提供了弱一致性的迭代器,從而保證在獲取迭代器後,其他線程對list的修改是不可見的,迭代器遍歷的數組是一個快照。