CopyOnWriteArrayList源碼學習

CopyOnWriteArrayList源碼學習

1.Copy-On-Write 策略

Copy-On-Write 簡稱COW,意思是寫入時複製;大家共享同一個變量,多個人讀取的時候都是那一個變量;當有一個人要修改變量,則先複製一份,修改變量的副本,這個過程中不影響其他人讀取原來的變量。
這種優化策略在Linux系統、Redis、文件系統中都有應用,這裏我們講解這種思想在Java中的應用。
CopyOnWrite容器,當向一個共享容器進行寫入或修改操作時,不直接操作原有容器,先拷貝一份新容器,在新容器中進行寫入或修改操作,最後將共享變量的地址指向新容器。這個過程中寫操作不影響併發讀,讀和寫是分離的。
Java中的CopyOnWriteArrayList和CopyOnWriteArraySet是對這種思想的實踐。

2. CopyOnWriteArrayList

在Java中要保證一個List線程安全,要麼使用Vector要麼使用Collections.synchronizedList方法生成一個線程安全的包裝類SynchronizedList。這兩種方式都是通過加鎖的方式實現了線程安全,區別在於Vector是在聲明方法的時候加synchronized,而SynchronizedList是在包裝方法內部添加synchronized實現。
這兩種方式有個共同的缺點,增刪改查方法都加鎖了,在讀多寫少的情況下很影響性能。

特點:

線程安全,無須手動加鎖;

通過鎖+數組拷貝+violate保證了線程安全;

2.1 構成

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;
    //....
}

CopyOnWriteArrayList實現了List接口,內部數據存儲和ArrayList同樣使用數組實現,這裏需要注意的是:

(1)這裏使用ReentrantLock作爲獨佔鎖,相比於synchronized,ReentrantLock更靈活,但需要手動釋放鎖。

(2)array 使用volatile修飾,保證了可見性,有最終一致性,但不是實時一致的。

2.2 添加元素

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

寫入操作的第一步是加鎖,這裏可以學習下ReentrantLock的使用,記得在finally語句中釋放鎖。剩下的就是拷貝數組,在新數組上添加元素,最後將array指向新數組。

這裏有個點需要注意下:這裏已經加鎖了,爲什麼不直接在原數組上修改?

一是修改原數組會導致讀取數據異常,二是violate修飾的是array變量,修改原數組雖然數組變了,但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)// 移動元素數爲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();
    }
}

這裏總結一下:

(1)加鎖保證了同一時刻,只能有一個線程進行修改操作。

(2)使用數組拷貝生成新的數組,在新數組上進行修改操作,不影響併發讀取數據的一致性。

(3)使用violate修飾array,指向新數組時,其他線程可以立即讀到最新值。

2.3 刪除元素

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

指定索引刪除元素的過程和按索引添加元素的過程類似,都是加鎖判斷位置進行拷貝最後解鎖。

修改元素的過程同樣也是加鎖後對複製的新數組進行修改,最後更改數組引用,這裏不貼代碼了,直接看迭代的代碼。

2.4 迭代過程

由於CopyOnWriteArrayList修改的都是老數組,所以對原數組在結構上時沒有修改的,在迭代過程中修改元素不會拋ConcurrentModificationException異常。但需要注意的時一旦迭代開始,迭代器持有的數組時不變的,此時修改數組不會影響迭代的順序。

static final class COWIterator<E> implements ListIterator<E> {
    // 迭代器持有的數組快照,
    private final Object[] snapshot;
    // 迭代的遊標
    private int cursor;
	// 創建迭代器時就決定了快照的版本
    private COWIterator(Object[] elements, int initialCursor) {
        cursor = initialCursor;
        snapshot = elements;
    }
    // 支持向後迭代
    public boolean hasNext() {
        return cursor < snapshot.length;
    }
    // 支持向前迭代
    public boolean hasPrevious() {
        return cursor > 0;
    }
    @SuppressWarnings("unchecked")
    public E next() {
        if (! hasNext())
            throw new NoSuchElementException();
        return (E) snapshot[cursor++];
    }
    @SuppressWarnings("unchecked")
    public E previous() {
        if (! hasPrevious())
            throw new NoSuchElementException();
        return (E) snapshot[--cursor];
    }
    public int nextIndex() {
        return cursor;
    }
    public int previousIndex() {
        return cursor-1;
    }
    // 不支持修改操作
    public void remove() {
        throw new UnsupportedOperationException();
    }
    public void set(E e) {
        throw new UnsupportedOperationException();
    }
    public void add(E e) {
        throw new UnsupportedOperationException();
    }
	// 遍歷操作同樣只是針對快照操作
    @Override
    public void forEachRemaining(Consumer<? super E> action) {
        Objects.requireNonNull(action);
        Object[] elements = snapshot;
        final int size = elements.length;
        for (int i = cursor; i < size; i++) {
            @SuppressWarnings("unchecked") E e = (E) elements[i];
            action.accept(e);
        }
        cursor = size;
    }
}

2.5 小結

這裏總結一下CopyOnWriteArrayList的特點,CopyOnWriteArrayList讀寫分離,適合讀多寫少的場景;使用ReentrantLock作爲獨佔鎖,保證增刪改操作的互斥性。當然缺點也很明顯,首先是佔用內存多,每次修改都會複製新數組;其次正因爲讀寫分離了,所以寫操作不具有實時一致性,只有最終一致性,如果對讀取數據有實時一致性要求則不要使用該容器;同樣迭代過程讀到的是快照數組中的值,同樣不具備實時性。

3.CopyOnWriteArraySet

CopyOnWriteArraySet內部使用的是CopyOnWriteArrayList實現。

引用:

聊聊併發-Java中的Copy-On-Write容器,by 方騰飛

CopyOnWriteArrayList你都不知道,怎麼拿offer?,by Java3y

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章