在我們需要保證線程安全的時候,如果使用到Map,那麼我們可以使用線程安全的ConcurrentHashMap,ConcurrentHashMap不僅可以保證線程安全,而且效率也非常不錯,那有沒有線程安全的List呢?答案是有,那就是CopyOnWriteArrayList。今天我們就一起來了解一下CopyOnWriteArrayList,看它是如何巧妙的保證線程安全的吧。
一:成員變量分析
//進行修改操作時的鎖
final transient Reentrantlock lock = new ReentrantLock();
//真正保存數據的數組 用volatile關鍵字進行修飾,保證array的引用的可見性
private transient volatile Object[] array;
二:源碼分析
首先我們看構造方法,CopyOnWriteArrayList有三個構造方法。
1.空參構造
調用setArray方法將成員變量array賦值為一個長度為0的數組。
public CopyOnWriteArrayList() {
setArray(new Object[0]);
}
final void setArray(Object[] a) {
array = a;
}
2.傳入一個Collection對象的構造方法
首先判斷Collection是否是一個CopyOnWriteArrayList,如果是,直接將傳入的CopyOnWriteArrayList的elements重新賦值給需要創建的CopyOnWriteArrayList。 如果不是,判斷Collection是否是ArrayList,如果是,那麼就利用toArray()方法將其轉化為一個數組並賦值給成員變量array,否則將Collection裡面的元素全部取出來copy到一個新數組中,並且將該數組賦值給成員變量array。
public CopyOnWriteArrayList(Collection<? extends E> c) {
Object[] elements;
if (c.getClass() == CopyOnWriteArrayList.class)
elements = ((CopyOnWriteArrayList<?>)c).getArray();
else {
elements = c.toArray();
if (c.getClass() != ArrayList.class)
elements = Arrays.copyOf(elements, elements.length, Object[].class);
}
setArray(elements);
}
3.傳入一個數組的構造方法
將傳入的數組的元素copy到一個新的Object數組,並且賦值給成員變量array。
public CopyOnWriteArrayList(E[] toCopyIn) {
setArray(Arrays.copyOf(toCopyIn, toCopyIn.length, Object[].class));
}
接下來我們看核心的add(),remove(),get()方法。
add(E e)
首先加鎖,然後通過Arrays.copyOf()方法將元素copy到一個新的數組中,新的數組的長度為原數組的長度+1,並且將需要加入的元素賦值到新數組的最後。最後將新數組賦值給成員變量array。
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(int index, E element)方法需要將元素加入到指定的索引位置中。首先也是先加鎖,保證線程安全,將原數組分為兩段進行操作,根據index進行分隔,分別copy index之前的元素和之後的元素,copy完成之後在將需要插入的元素設置到索引為index的位置上。之後將新數組賦值給成員變量array。
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];
System.arraycopy(elements, 0, newElements, 0, index);
System.arraycopy(elements, index, newElements, index + 1,
numMoved);
}
newElements[index] = element;
setArray(newElements);
} finally {
lock.unlock();
}
}
接下來看remove()方法。
remove(int index)
remove(int index)方法需要在數組中移除指定索引的值。首先是加鎖,同樣也是將原數組分為兩段進行操作,根據index進行分隔,分別copy index之前的元素和之後的元素,copy到一個新數組中,新數組的長度為原數組的長度減一(注意這裡是沒有copy index索引位置的值的,所以相當於移除了index索引上的值)。之後將新數組賦值給成員變量array。
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();
}
}
接下來看get()方法
get()
我們可以看到get()方法很簡單,就是從array成員變量中取出對應索引的值。並沒有加鎖處理。所以儘管是在並發高的情況下,get()方法的效率依舊是比較高的。
/**
* {@inheritDoc}
*
* @throws IndexOutOfBoundsException {@inheritDoc}
*/
public E get(int index) {
return get(getArray(), index);
}
private E get(Object[] a, int index) {
return (E) a[index];
}
三:總結
CopyOnWriteArrayList為什麼能夠保證線程安全,主要是因為以下幾點:
- 在做修改操作的時候加鎖
- 每次修改都是將元素copy到一個新的數組中,並且將數組賦值到成員變量array中。
- 利用volatile關鍵字修飾成員變量array,這樣就可以保證array的引用的可見性,每次修改之前都能夠拿到最新的array引用。這點很關鍵。
看到這裡,相信你已經對CopyOnWriteArrayList非常了解了,CopyOnWriteArrayList在查詢多,修改操作少的情況下效率是非常可觀的,既能夠保證線程安全,又能有不錯的效率。但是如果修改操作較多,就會導致數組頻繁的copy,效率就會有所下降,如果修改操作很多,那麼直接使用Collections.synchronizedList(),或許也是一個不錯的選擇。
作者:沉迷學習的小伙
連結:https://juejin.cn/post/7143428003777740814
來源:稀土掘金