一、併發List簡答介紹
- 因爲併發包中只有CopyOnWriteArrayList ,CopyOnWriteArrayList是一個線程安全的ArrayList,對其進行的修改操作都是在底層的一個複製的數組(快照)上進行的,也就是使用了寫時複製策略。
final transient ReentrantLock lock = new ReentrantLock();
private transient volatile Object[] array;
CopyOnWriteArrayList 對象裏面有一個array數組對象用來存放具體元素,ReentrantLock 獨佔鎖對象用來保證同時只有一個線程對array進行修改。
- 寫時複製的線程安全的list需要注意以下幾點:
何時初始化list,初始化的list 元素個數爲多少,list是有限大小嗎?
如何保證線程安全,比如多個線程進行讀寫時如何保證是線程安全的?
如何保證使用迭代器遍歷List時的數據一致性?
二、源碼解析
1、初始化:
- 無參構造函數
public CopyOnWriteArrayList() {
setArray(new Object[0]);
}
代碼內部創建了一個大小爲 0 的 Object數組作爲Array的初始值。
- 有參構造函數:
public CopyOnWriteArrayList(E[] toCopyIn) {
setArray(Arrays.copyOf(toCopyIn, toCopyIn.length, Object[].class));
}
創建一個 List 其內部元素是入參toCopyIn的副本
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);
}
入參是一個集合,將集合裏的元素複製到本List
2、添加元素:
- 上圖中的原理類似主要看一下add(E e)
public boolean add(E e) {
//獲取獨佔鎖
final ReentrantLock lock = this.lock;
lock.lock();
try {
//獲取array
Object[] elements = getArray();
int len = elements.length;
//複製array 到新數組,
Object[] newElements = Arrays.copyOf(elements, len + 1);
//添加元素到新數組
newElements[len] = e;
//使用新數組替換添加前的數組
setArray(newElements);
return true;
} finally {
//釋放獨佔鎖
lock.unlock();
}
}
//獲取數組array
final Object[] getArray() {
return array;
}
//替換數組
final void setArray(Object[] a) {
array = a;
}
調用add 方法的線程會首先執行代碼中獲取獨佔鎖的部分,進行鎖的獲取,如果duoge線程都調用add 方法則只有一個線程會獲取到該鎖,其他線程會被阻塞掛起直到鎖被釋放。
所以一個線程獲取到鎖後,就保證了在該線程添加元素的過程中其他線程不會對array進行修改。
獲取鎖後,獲取數組,獲取到數組後,將該數組複製到一個新的數組【將原來的數組長度➕1】,將新增的元素添加到新數組。
使用新數組替換原數組,並在返回前釋放鎖,由於加了鎖,整個add 過程是個原子性的操作,【注意: 添加元素的時候,是在 副本上進行的操作,而不是在原數組上進行操作】。
3、獲取指定位置元素:
/**
* {@inheritDoc}
*
* @throws IndexOutOfBoundsException {@inheritDoc}
*/
public E get(int index) {
return get(getArray(), index);
}
- getArray 和上面的一樣的,看一下 get方法:
@SuppressWarnings("unchecked")
private E get(Object[] a, int index) {
return (E) a[index];
}
使用get(int index) 獲取下標爲index 的元素,如果元素不存在則拋出IndexOutOfBoundsException異常。
當線程X 調用get 方法獲取指定位置的元素時,分兩步: 1、首先獲取array數組【一】,2、通過下標訪問指定位置的元素【2】。但是我們要注意到整個過程並沒有加鎖同步。
因爲這兩部都沒有加鎖,可能線程X執行完步驟一 之後,在步驟2 之前,另外一個線程y 進行了remove 操作,假設刪除元素1 ,remove會首先獲取獨佔鎖,然後進行寫時複製操作,也就是複製一份當前array數組,然後在複製的數組裏刪除 1 這個元素,之後讓array 指向複製的數組,而此時,array 之前指向的數組的引用計數器爲1 而不是0 ,因爲線程X 還在使用它,這時線程X 開始執行步驟2,步驟2 操作的數組是 線程Y 刪除元素之前的數組。
- 所以雖然Y 已經刪除了index 處的元素,但是線程X的步驟還是會返回index 處的鎖,這是寫時複製策略產生的弱一致性問題。
4、修改指定元素:
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();
}
}
首先獲取獨佔鎖,從而阻止其他線程對array數組進行修改,然後獲取當前數組,並調用get方法獲取指定位置的元素,如果指定位置的元素值與新值不一致則創建新數組並複製元素,然後在新數組上修改指定位置的元素值並設置新數組到array,如果指定位置的元素值與新值一樣,則爲了保證volatile的語義,還是需要重新設置array,雖然array 的內容並沒有改變。
5、刪除元素:
- 刪除list中指定元素可以使用上圖中的方法,原理大致一樣,主要講一下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進行修改,然後獲取數組中要被刪除的元素,並把剩餘的元素複製到新數組,之後使用新數組替換原來的老數組,最後在返回前釋放鎖。
6、弱一致性的迭代器:
所謂的弱一致性是指返回迭代器後,其他線程對List的刪除改對迭代器是不可見的。前面在獲取指定位置的元素試提到過。
使用iterator獲取迭代器時會返回COWIterator 對象,COWIterator 存儲着一個array的快照,snapshot,說snapshot是list的快照的原因是:遍歷元素的過程中,其他線程沒有對list進行增刪改,那麼snapshot本身就是list的array,因爲它們是引用關係,但是如果遍歷期間其他線程對該list進行了增刪改,那麼snapshot就是快照了,因爲增刪改後,list裏的數組被新數組替代了,這時候老數組被snapshot引用。
這就說明獲取迭代器後,使用該迭代器元素時,其他線程對該list進行的增刪改不可見。因爲操作的其實是兩個數組,這就是弱一致性。
- 這個地方和之前的獲取指定位置的元素很像。
三、總結
CopyOnWriteArrayList 使用寫時複製的策略來保證list的一致性,而 獲取 - 修改 - 寫入 這三個步驟並不是原子的,所以在增刪改的過程中都使用了獨佔鎖,來保證某個某個時間內只有一個線程對list數組進行修改。CopyOnWriteArrayList 還提供了 弱一致性的迭代器從而保證在獲取迭代器後,其他線程對list的修改是不可見的。迭代器遍歷的數組是一個快照。
CopyOnWriteArraySet 底層使用 CopyOnWriteArrayList 實現的。