1. 程式人生 > >併發容器學習—CopyOnWriteArrayList與CopyOnWriteArraySet

併發容器學習—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,因此不再做過多的分析。