本文首發於一世流雲專欄: https://segmentfault.com/blog...
ArrayList
是一種「列表」數據機構,其底層是經過數組來實現元素的隨機訪問。JDK1.5以前,若是想要在併發環境下使用「列表」,通常有如下3種方式:java
Collections.synchronizedList
返回一個同步代理類;前兩種方式都至關於加了一把「全局鎖」,訪問任何方法都須要首先獲取鎖。第3種方式,須要本身實現,複雜度較高。segmentfault
JDK1.5時,隨着J.U.C引入了一個新的集合工具類——CopyOnWriteArrayList
:數組
大多數業務場景都是一種「讀多寫少」的情形,CopyOnWriteArrayList就是爲適應這種場景而誕生的。併發
CopyOnWriteArrayList,運用了一種「寫時複製」的思想。通俗的理解就是當咱們須要修改(增/刪/改)列表中的元素時,不直接進行修改,而是先將列表Copy,而後在新的副本上進行修改,修改完成以後,再將引用從原列表指向新列表。dom
這樣作的好處是讀/寫是不會衝突的,能夠併發進行,讀操做仍是在原列表,寫操做在新列表。僅僅當有多個線程同時進行寫操做時,纔會進行同步。工具
CopyOnWriteArrayList的字段很簡單:大數據
public class CopyOnWriteArrayList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable { /** * 排它鎖, 用於同步修改操做 */ final transient ReentrantLock lock = new ReentrantLock(); /** * 內部數組 */ private transient volatile Object[] array; }
其中,lock
用於對修改操做進行同步,array
就是內部實際保存數據的數組。this
構造器定義spa
CopyOnWriteArrayList提供了三種不一樣的構造器,這三種構造器最終都是建立一個數組,並經過setArray
方法賦給array
字段:線程
/** * 空構造器. */ public CopyOnWriteArrayList() { setArray(new Object[0]); } 僅僅是設置一個了大小爲0的數組,並賦給字段array: final void setArray(Object[] a) { array = a; }
/** * 根據已有集合建立 */ 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); }
/** * 根據已有數組建立. * * @param toCopyIn the array (a copy of this array is used as the * internal array) * @throws NullPointerException if the specified array is null */ public CopyOnWriteArrayList(E[] toCopyIn) { setArray(Arrays.copyOf(toCopyIn, toCopyIn.length, Object[].class)); }
查詢——get方法
public E get(int index) { return get(getArray(), index); } private E get(Object[] a, int index) { return (E) a[index]; }
能夠看到,get方法並無加鎖,直接返回了內部數組對應索引位置的值:array[index]
添加——add方法
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); // 內部array引用指向新數組 return true; } finally { lock.unlock(); } }
add方法首先會進行加鎖,保證只有一個線程能進行修改;而後會建立一個新數組(大小爲n+1
),並將原數組的值複製到新數組,新元素插入到新數組的最後;最後,將字段array
指向新數組。
上圖中,ThreadB對Array的修改因爲是在新數組上進行的,因此並不會對ThreadA的讀操做產生影響。
刪除——remove方法
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) // index位置恰好是最後一個元素 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 int size() { return getArray().length; } public boolean isEmpty() { return size() == 0; }
迭代
CopyOnWriteArrayList對元素進行迭代時,僅僅返回一個當前內部數組的快照,也就是說,若是此時有其它線程正在修改元素,並不會在迭代中反映出來,由於修改都是在新數組中進行的。
public Iterator<E> iterator() { return new COWIterator<E>(getArray(), 0); } static final class COWIterator<E> implements ListIterator<E> { /** * Snapshot of the array */ private final Object[] snapshot; /** * Index of element to be returned by subsequent call to next. */ private int cursor; private COWIterator(Object[] elements, int initialCursor) { cursor = initialCursor; snapshot = elements; } public boolean hasNext() { return cursor < snapshot.length; } public E next() { if (!hasNext()) throw new NoSuchElementException(); return (E) snapshot[cursor++]; } // ... }
能夠看到,上述iterator方法返回一個迭代器對象——COWIterator
,COWIterator的迭代是在舊數組上進行的,當建立迭代器的那一刻就肯定了,因此迭代過程當中不會拋出併發修改異常——ConcurrentModificationException
。
另外,迭代器對象也不支持修改方法,所有會拋出UnsupportedOperationException
異常。
CopyOnWriteArrayList的思想和實現總體上仍是比較簡單,它適用於處理「讀多寫少」的併發場景。經過上述對CopyOnWriteArrayList的分析,讀者也應該能夠發現該類存在的一些問題:
1. 內存的使用
因爲CopyOnWriteArrayList使用了「寫時複製」,因此在進行寫操做的時候,內存裏會同時存在兩個array數組,若是數組內存佔用的太大,那麼可能會形成頻繁GC,因此CopyOnWriteArrayList並不適合大數據量的場景。
2. 數據一致性CopyOnWriteArrayList只能保證數據的最終一致性,不能保證數據的實時一致性——讀操做讀到的數據只是一份快照。因此若是但願寫入的數據能夠馬上被讀到,那CopyOnWriteArrayList並不適合。