併發和多執行緒(十七)--CopyOnWriteArrayList原始碼解析
我們肯定都使用過ArrayList,但是多執行緒或併發環境下,ArrayList作為共享變數被訪問,是執行緒不安全的。我們可以選擇自己加鎖或Collections.synchronizedList()去實現一個執行緒安全的容器。除此之外,我們還可以使用今天要學習的CopyOnWriteArrayList。
CopyOnWrite:
是一種策略,表示在使用的時候,將共享內容拷貝一份進行修改,修改完成之後,然後將原容器指向修改後的內容,保證的是最終一致性,因為讀寫是不同的容器,讀的時候不加鎖,寫的時候加鎖。JDK中有CopyOnWriteArrayList和CopyOnWriteArraySet兩種已經實現的容器,瞭解這種思想,我們甚至可以自定義實現別的容器,例如Map,但是為什麼沒有實現CopyOnWriteHashMap呢,因為已經有了優秀的ConcurrentHashMap。
類註釋:
從類註釋我們可以得到以下資訊:
- CopyOnWriteArrayList是ArrayList的安全變體。
- CopyOnWriteArrayList的實現需要陣列拷貝有一定的成本,但是通過比其他的方案效率更高。
- 允許存放各種元素,包括null。
- 迭代過程中,進行修改不會出現ConcurrentModificationException,因為讀寫分離。
類屬性:
public class CopyOnWriteArrayList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable { private static final long serialVersionUID = 8673264195747942595L; final transient ReentrantLock lock = new ReentrantLock(); private transient volatile Object[] array; final Object[] getArray() { return array; } final void setArray(Object[] a) { array = a; } public CopyOnWriteArrayList() { setArray(new Object[0]); } 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)); } }
從上面的程式碼可以得到以下資訊:
- CopyOnWriteArrayList有一個ReentrantLock實現的鎖。
- 和ArrayList一樣,都是通過陣列儲存資料,但是多個volatile的修飾,用來滿足Happen-Before原則。
- 預設建構函式,將陣列初始化為1,沒有設定陣列大小的建構函式,只能講陣列或者容器傳入。
新增
public void add(int index, E element) { final ReentrantLock lock = this.lock; //通過ReentrantLock加鎖 lock.lock(); try { //得到陣列 Object[] elements = getArray(); int len = elements.length; //判斷陣列下標是否越界 if (index > len || index < 0) throw new IndexOutOfBoundsException("Index: "+index+ ", Size: "+len); Object[] newElements; int numMoved = len - index; //如果直接插入到尾部,直接使用copyOf複製一個新的陣列,length+1 if (numMoved == 0) newElements = Arrays.copyOf(elements, len + 1); else { //直接初始化一個新的陣列,長度+1 newElements = new Object[len + 1]; //複製index前面的部分到新陣列 System.arraycopy(elements, 0, newElements, 0, index); //複製index後面的部分到新陣列的相應位置 System.arraycopy(elements, index, newElements, index + 1, numMoved); } //對index位置賦值 newElements[index] = element; //設定到陣列中 setArray(newElements); } finally { //解鎖 lock.unlock(); } }
這裡選擇了插入對應下標的方法,如果是插入到尾部,更加簡單,可以自行檢視。和ArrayList的相關原始碼差不多,除了加鎖解鎖,就是ArrayList是直接對原陣列進行修改,而CopyOnWriteArrayList選擇生成一個新的陣列,先將elements的index前面部分複製過去,然後複製後面的部分,然後將element賦值,整體程式碼非常簡單明瞭。
System.arraycopy()
引數分別為:原陣列,複製的起始下標,新陣列,複製到新陣列原則的起始下標,複製的長度。
獲取
public E get(int index) {
return get(getArray(), index);
}
private E get(Object[] a, int index) {
return (E) a[index];
}
get()沒啥好講的,就是從陣列中獲取,可以看到沒有通過鎖進行加密,讀的時候不進行加鎖,很有可能出現讀到舊資料的情況。
刪除
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();
}
}
步驟如下:
- 加鎖。
- 通過下標獲取oldValue。
- 如果index為最後一位,直接複製資料,長度-1.
- 如果index不是最後一位,生成一個新陣列,然後先複製index前面的部分,然後index後面的資料整體遷移一位。
- 解鎖。
修改
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;
Object[] newElements = Arrays.copyOf(elements, len);
newElements[index] = element;
setArray(newElements);
} else {
// Not quite a no-op; ensures volatile write semantics
setArray(elements);
}
return oldValue;
} finally {
lock.unlock();
}
}
步驟如下:
- 加鎖。
- 通過引數下標獲取oldValue。
- 如果oldValue和引數element不等,生成一個新陣列,然後直接替換index位置的資料,setArray()。
- 否則setArray(),這一步沒看太懂,看了上面的註釋,確保volatile的寫語義。
- 返回oldValue,解鎖。
批量刪除
public boolean removeAll(Collection<?> c) {
if (c == null) throw new NullPointerException();
final ReentrantLock lock = this.lock;
lock.lock();
try {
Object[] elements = getArray();
int len = elements.length;
if (len != 0) {
int newlen = 0;
Object[] temp = new Object[len];
for (int i = 0; i < len; ++i) {
Object element = elements[i];
if (!c.contains(element))
temp[newlen++] = element;
}
if (newlen != len) {
setArray(Arrays.copyOf(temp, newlen));
return true;
}
}
return false;
} finally {
lock.unlock();
}
}
步驟如下:
- 加鎖。
- 當前陣列為空,返回false,否則繼續。
- 定義變數newlen,和一個長度為len的空陣列,遍歷elements陣列,判斷要刪除的collection中是否當前下標值,如果不包含儲存到temp陣列。
- 如果newlen != len,複製一個長度為newlen的陣列,setArray(),佛足額返回false。
- 解鎖。
從上面我們看到批量刪除並不是遍歷然後刪除每個元素,因為刪除元素每次都會有一次陣列拷貝,這樣會很消耗效能,加鎖時間變得很長,影響併發,所以這是批量刪除更好的思路,將不需要刪除的元素都放到新陣列中,而不是挨個刪除。
關於迭代:
CopyOnWriteArrayList通過COWIterator實現的迭代器,每次迭代,持有的是原陣列的引用,所以修改和新增不會影響到迭代。
總結:
通過上面的原始碼,我們隊CopyOnWriteArrayList有了基本的瞭解,由於讀的時候不加鎖,而synchronizedList讀寫都是加鎖的,所以相對來說還是比較推薦使用前者,在併發多執行緒的場景下。