1. 程式人生 > 其它 >寫時複製,CopyOnWriteArrayList原始碼剖析

寫時複製,CopyOnWriteArrayList原始碼剖析

寫時複製,CopyOnWriteArrayList原始碼剖析

1、CopyOnWriteArrayList介紹

CopyOnWriteArrayList是一個執行緒安全的ArrayList,對其進行的修改操作都是在底層的一個複製的陣列(快照)上進行的,也就是使用了寫時複製策略。

每個 CopyOnWriteArrayList物件裡面有一個array陣列物件用來存放具體元素,ReentrantLock獨佔鎖物件用來保證同時只有一個執行緒對array進行修改。

寫時複製,CopyOnWrite容器即寫時複製的容器,往一個容器中新增元素的時候,不直接往當前容器Object[]新增,而是先將Object[]進行copy,複製出一個新的容器object[] newElements,然後新的容器Object[] newElements裡新增原始,新增元素完後,在將原容器的引用指向新的容器 setArray(newElements);這樣做的好處是可以對copyOnWrite容器進行併發的讀 ,而不需要加鎖,因為當前容器不需要新增任何元素。所以CopyOnWrite容器也是一種讀寫分離的思想,讀和寫不同的容器。

2、CopyOnWriteArrayList原始碼剖析

初始化
public class CopyOnWriteArrayList<E>
    implements List<E>, RandomAccess, Cloneable, java.io.Serializable {
    private static final long serialVersionUID = 8673264195747942595L;

    /** The lock protecting all mutators */
    final transient ReentrantLock lock = new ReentrantLock();

    /** The array, accessed only via getArray/setArray. */
    private transient volatile Object[] array;
}

構造一個Object型別陣列

final void setArray(Object[] a) {
    array = a;
}

無參建構函式,在內部建立了一個大小為0的Object陣列作為array的初始值。

public CopyOnWriteArrayList() {
    setArray(new Object[0]);
}

入參為集合,將集合裡面的元素複製到本list

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);
}

建立一個list,其內部元素是入參toCopyIn的副本

public CopyOnWriteArrayList(E[] toCopyIn) {
    setArray(Arrays.copyOf(toCopyIn, toCopyIn.length, Object[].class));
}
新增元素
public boolean add(E e) {
    // 獲取獨佔鎖
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
        // 獲取array
        Object[] elements = getArray();
        // 獲取array的陣列長度,並將其複製到新陣列newElements中,陣列長度+1,將新添的值新增到新陣列中
        int len = elements.length;
        Object[] newElements = Arrays.copyOf(elements, len + 1);
        newElements[len] = e;
        // 使用新陣列替換新增前的陣列
        setArray(newElements);
        return true;
    } finally {
        lock.unlock();
    }
}

如上程式碼所示,呼叫add方法的程式碼會首先去獲取獨佔鎖lock,如果多個執行緒都呼叫add方法,則只有一個執行緒會獲取到該鎖,其他執行緒會被阻塞掛起,直到鎖被釋放。所以一個執行緒獲取到鎖後,就保證了該執行緒新增元素的過程中其他執行緒不會對array進行修改。

執行緒獲取鎖後再執行程式碼獲取陣列array,然後複製array到一個新陣列,新陣列的大小是原來陣列的大小加1,所以CopyOnWriteList是一個無界List,並把新增的元素新增到新陣列。

然後使用新陣列替換原陣列,並在返回前釋放鎖,由於加了鎖,所以整個add過程是個原子性操作。再新增元素時,首先複製了一個快照,然後在快照上進行新增,而不是直接在原來陣列上進行。

// 返回array陣列
final Object[] getArray() {
    return array;
}
獲取指定位置元素
public E get(int index) {
    return get(getArray(), index);
}
private E get(Object[] a, int index) {
    return (E) a[index];
}
final Object[] getArray() {
    return array;
}

由於get操作沒有加鎖,所以最終拿到的資料不一定是最新的資料。在如上程式碼中,執行緒x呼叫get方法獲取指定位置的元素時分兩步走,首先獲取array陣列,然後再通過下標訪問指定位置的元素,如果在獲取指定位置元素前,有另外一個執行緒y對陣列進行了刪除操作,則這時執行緒x還是隻想原始陣列array,而此時堆記憶體的陣列已經變為了newarray,所以這就是寫時複製策略產生的弱一致性問題

修改指定元素
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();
    }
}

如上程式碼,首先獲取獨佔鎖lock,從而組織了其他執行緒對array陣列進行修改、新增、刪除等操作,然後獲取到當前陣列,並呼叫get方法獲取指定位置的元素,如果指定位置的元素值和新值不一致則建立新陣列並複製元素,然後新陣列上修改指定位置的元素值並設定新陣列到array。如果指定位置的元素值與新值一樣,則為了保證volatile語義,還需要重新設定array,雖然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();
    }
}

刪除元素程式碼和新增元素的程式碼類似,首先獲取獨佔鎖以保證刪除資料期間其他執行緒不能對array進行修改,然後獲取陣列中要被刪除的元素,並把剩餘的元素複製到新陣列,之後使用新陣列替換原來的陣列,最後在返回前釋放。