簡介
我們都很熟悉容器對象ArrayList,並且在初學時就被告知ArrayList不是線程安全的:當我們在使用迭代器遍歷ArrayList時,如果有其他線程修改了ArrayList對象,那么就會拋出ConcurrentModificationException異常。相較於Vector使用synchronized加鎖保證線程安全性,JUC提供了多線程版“ArrayList”:CopyOnWriteArrayList。下面是JDK對CopyOnWriteArrayList的介紹:
A thread-safe variant of ArrayList in which all mutative operations (add, set, and so on) are implemented by making a fresh copy of the underlying array.
This is ordinarily too costly, but may be more efficient than alternatives when traversal operations vastly outnumber mutations, and is useful when you cannot or don't want to synchronize traversals, yet need to preclude interference among concurrent threads. The "snapshot" style iterator method uses a reference to the state of the array at the point that the iterator was created. This array never changes during the lifetime of the iterator, so interference is impossible and the iterator is guaranteed not to throw ConcurrentModificationException. The iterator will not reflect additions, removals, or changes to the list since the iterator was created. Element-changing operations on iterators themselves (remove, set, and add) are not supported. These methods throw UnsupportedOperationException.
大意是CopyOnWriteArrayList是線程安全版本的ArrayList,所有對CopyOnWriteArrayList修改的操作都是在內部數組的拷貝上進行操作的,這樣做雖然內存花費大,但是在遍歷操作大於修改操作時這樣效率更高,可以有效防止拋出ConcurrentModificationException異常,迭代器迭代期間不支持對元素更改操作,否則會拋出UnsupportedOperationException異常。
類結構
CopyOnWriteArrayList實現了List接口,List表示是有序的Collection,即它用某種特定的插入順序來維護元素順序;實現了標記接口RandomAccess接口支持快速訪問;實現了Iterable接口可以使用迭代器遍歷容器元素。
源碼解析
構造方法
CopyOnWriteArrayList互斥鎖用於對修改容器元素階段加鎖,被volatile修飾的Object數組是CopyOnWriteArrayList存儲數據的底層數據結構,通過volatile保證能夠讀到其他線程對CopyOnWriteArrayList數據的修改,對於數組的訪問都是通過getArray/setArray方法。
CopyOnWriteArrayList提供了三個重載的構造函數,無參構造函數會調用setArray方法構造一個空的Object數組,另外兩個構造函數分別傳入集合/數組參數,將集合/數組內元素存入CopyOnWriteArrayList的底層Object數組。
public class CopyOnWriteArrayList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable {
private static final long serialVersionUID = 8673264195747942595L;
//互斥鎖
final transient ReentrantLock lock = new ReentrantLock();
//底層存儲數據數組,只能通過getArray/setArray訪問設置,volatile動態數組
private transient volatile Object[] array;
final Object[] getArray() {
return array;
}
final void setArray(Object[] a) {
array = a;
}
public CopyOnWriteArrayList() {
setArray(new Object[0]);
}
//傳入Collection集合對象,將集合中元素存入CopyOnWriteArrayList
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);
}
//傳入數組
public CopyOnWriteArrayList(E[] toCopyIn) {
setArray(Arrays.copyOf(toCopyIn, toCopyIn.length, Object[].class));
}
}
add(E e)
add方法的作用是把傳入元素添加到鏈表list的末尾。add方法有兩點需要注意:1.在寫入過程使用了互斥鎖,所以同一時間只有一個線程在修改CopyOnWriteArrayList 2.增加元素並不是直接在原數組操作,而是在原數組的拷貝數組上添加元素的,添加完成后再調用setArray方法用新數組代替原始數組
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();
}
}
add(int index, E element)
這個方法的作用是把新元素插入到特定位置,會把原來位置的元素向后擠。過程與上面的add大致相同。
public void add(int index, E element) {
//互斥鎖
final ReentrantLock lock = this.lock;
lock.lock();
try {
//原始數組
Object[] elements = getArray();
int len = elements.length;
//檢查index有效性
if (index > len || index < 0)
throw new IndexOutOfBoundsException("Index: "+index+
", Size: "+len);
//拷貝數組
Object[] newElements;
//從index到數組末尾要向后移動一位數組元素的個數
int numMoved = len - index;
//如果index==length,直接把原數組復制到新數組
if (numMoved == 0)
newElements = Arrays.copyOf(elements, len + 1);
//否則分成兩段復制,原始數組index前面的元素位置一一對應賦值到新數組,原數組index開始的元素復制到
//新數組index+1到length+1,相當於依次后移。空出來的index就是新元素插入的位置
else {
newElements = new Object[len + 1];
System.arraycopy(elements, 0, newElements, 0, index);
System.arraycopy(elements, index, newElements, index + 1,
numMoved);
}
//插入新元素
newElements[index] = element;
setArray(newElements);
} finally {
lock.unlock();
}
}
get(int index)
獲取索引位置為index位置處的元素,獲取元素過程中沒有使用互斥鎖上鎖。
public E get(int index) {
return get(getArray(), index);
}
@SuppressWarnings("unchecked")
private E get(Object[] a, int index) {
//返回數組index處位置
return (E) a[index];
}
remove(int index)
移除index處元素。由於涉及到修改到對鏈表內元素的修改,因此移除過程會使用互斥鎖上鎖。
public E remove(int index) {
//上鎖
final ReentrantLock lock = this.lock;
lock.lock();
try {
//原始數組
Object[] elements = getArray();
int len = elements.length;
//數組index處要移除的元素
E oldValue = get(elements, index);
//index+1到數組末尾要移動的元素個數
int numMoved = len - index - 1;
//如果要移除的元素在數組末尾(index=len-1),直接復制數組區間[0,len-2]所有元素到新數組
if (numMoved == 0)
setArray(Arrays.copyOf(elements, len - 1));
//如果移除的元素不再末尾,分成兩段賦值,首先把[0,index-1]區間元素復制到新數組,再把
//[index+1,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();
}
}
迭代器Iterator遍歷
CopyOnWriteArrayList支持使用迭代器迭代,使用iterator方法返回COWIterator對象,在迭代過程中沒有上鎖,也不支持remove/set/add等修改方法。
//返回COWIterator對象
public Iterator<E> iterator() {
return new COWIterator<E>(getArray(), 0);
}
//實現迭代器的內部類
static final class COWIterator<E> implements ListIterator<E> {
//遍歷時原始數組的快照
private final Object[] snapshot;
//迭代器迭代的游標
private int cursor;
private COWIterator(Object[] elements, int initialCursor) {
cursor = initialCursor;
snapshot = elements;
}
public boolean hasNext() {
return cursor < snapshot.length;
}
public boolean hasPrevious() {
return cursor > 0;
}
@SuppressWarnings("unchecked")
public E next() {
if (! hasNext())
throw new NoSuchElementException();
return (E) snapshot[cursor++];
}
@SuppressWarnings("unchecked")
public E previous() {
if (! hasPrevious())
throw new NoSuchElementException();
return (E) snapshot[--cursor];
}
public int nextIndex() {
return cursor;
}
}
總結
Copy-On-Write簡稱COW,中文簡稱寫入時復制,是一種程序設計優化策略,具體思想就是對於共享內容做修改操作時,會把共享內容復制出來,在復制內容上修改,修改完成后在返還到原內容上。對於CopyOnWriteArrayList而言,向容器添加元素是先把容器復制一份,向復制的容器添加元素,添加成功后把復制的容器賦值給原容器對象;而對於讀取容器的操作直接在原容器進行操作。CopyOnWriteArrayList利用了COW技術實現讀寫的分離,對於寫操作實行加鎖保證安全性,讀操作不改變容器不需加鎖,相對於Vector對所有操作加鎖來保證安全性的效率更高,適合於讀多寫少的場景。同時在CopyOnWriteArrayList保存數據量較大時,對於容器的寫入由於復制原容器產生新容器用於寫操作,造成了兩倍的內存消耗,會引發頻繁的垃圾回收,降低性能。