併發容器學習—CopyOnWriteArrayList與CopyOnWriteArraySet
一、CopyOnWriteArrayList併發容器
1.CopyOnWriteArrayList的底層資料結構
CopyOnWriteArrayList的底層實現是陣列,CopyOnWriteArrayList是ArrayList的一個執行緒安全的變體,其增刪改操作都是通過對底層陣列的重新複製來實現的。這種容器記憶體開銷很大,但是在讀操作頻繁(查詢),寫操作(增刪改)極少的場合卻極用,並且每一次寫操作都是在一塊新的空間上進行的,不存在併發問題。
2.CopyOnWriteArrayList的繼承關係
CopyOnWriteArrayList繼承關係如下圖所示,List介面和Collection介面在學習ArrayList時已經分析過,不在多說。
3.重要屬性和構造方法
public class CopyOnWriteArrayList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable { //可重入鎖 final transient ReentrantLock lock = new ReentrantLock(); //底層實現,用於存放資料的陣列 private transient volatile Object[] array; //獲取陣列 final Object[] getArray() { return array; } //修改陣列 final void setArray(Object[] a) { array = a; } //預設構造器,初始化一個長度為0 的陣列 public CopyOnWriteArrayList() { setArray(new Object[0]); } //將集合c中所有元素轉化成陣列的形式,儲存與當前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); } //將陣列toCopyIn拷貝以一份到array中 public CopyOnWriteArrayList(E[] toCopyIn) { setArray(Arrays.copyOf(toCopyIn, toCopyIn.length, Object[].class)); } }
4.add與set過程
CopyOnWriteArrayList在新增元素時,會先進行加鎖操作,以保證寫操作的併發安全
//將新增資料加到陣列末尾 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(); //解鎖 } } //新增元素到指定位置 public void add(int index, E element) { final ReentrantLock lock = this.lock; //獲取重入鎖 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; //判斷插入的位置是否是陣列末尾 if (numMoved == 0) newElements = Arrays.copyOf(elements, len + 1); else { newElements = new Object[len + 1]; //將原陣列中0到index的元素複製到新陣列的0到index位置 System.arraycopy(elements, 0, newElements, 0, index); //將原陣列的index之後的資料複製到新陣列的index+1位置之後 System.arraycopy(elements, index, newElements, index + 1, numMoved); } newElements[index] = element; //新增element setArray(newElements); } finally { lock.unlock(); } } //修改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(); } }
5.get的過程
CopyOnWriteArrayList的讀取過程並沒加鎖,這是因為CopyOnWriteArrayList的讀過程是天然執行緒安全的,所有的寫操作都是在一塊新的記憶體空間上,而讀操作則是在原有的空間上進行的,不會出現併發問題,讀寫天然分離。
public E get(int index) {
return get(getArray(), index);
}
private E get(Object[] a, int index) {
return (E) a[index];
}
6.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)
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();
}
}
//將o從陣列中刪除
public boolean remove(Object o) {
Object[] snapshot = getArray();
int index = indexOf(o, snapshot, 0, snapshot.length); //獲取o的索引位置
//判斷index是否合法
return (index < 0) ? false : remove(o, snapshot, index);
}
//將snapshot中index位置的資料o刪除
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: {
//比較修改後的陣列長度與index索引的大小,取小的值
int prefix = Math.min(index, len);
//遍歷current陣列查詢第一個與快照陣列元素不同的索引
for (int i = 0; i < prefix; i++) {
if (current[i] != snapshot[i] && eq(o, current[i])) {
index = i;
break findIndex;
}
}
//o元素索引大於等於current陣列的長度
//說明current陣列中不存在o元素
if (index >= len)
return false;
//o元素的索引仍在current陣列中
//且要刪除元素的索引的沒有發生改變
if (current[index] == o)
break findIndex;
//o元素索引發生改變,重新獲取o元素的索引
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();
}
}
7.總結
由上面的分析可得出CopyOnWriteArrayList具有如下特點:
1.CopyOnWriteArrayList是執行緒安全的容器,讀寫分離,寫資料時通過ReentrantLock鎖定一個執行緒拷貝陣列元素進行增刪改操作;讀資料時則不需要同步控制。
2.CopyOnWriteArrayList中是允許null資料和重複資料的
3.CopyOnWriteArrayList的併發安全性是通過建立新的陣列和重入鎖實現的,會耗費大量記憶體空間,只適合讀多謝寫少,資料量不大的環境。
二、CopyOnWriteArraySet併發容器
1.CopyOnWriteArraySet
由於CopyOnWriteArraySet的底層是通過CopyOnWriteArrayList來實現的,其特點與CopyOnWriteArrayList,因此不再做過多的分析。