JDK併發工具類源碼學習系列——CopyOnWriteArrayList

CopyOnWriteArrayList是ArrayList的一個線程安全的變體,其中所有可變操作(add、set 等等)都是通過對底層數組進行一次新的複製來實現的。 這一般需要很大的開銷,但是當遍歷操作的數量大大超過可變操作的數量時,這種方法可能比其他替代方法更 有效。在不能或不想進行同步遍歷,但又需要從併發線程中排除衝突時,它也很有用。“快照”風格的迭代器方法在創建迭代器時使用了對數組狀態的引用。此數組在迭代器的生存期內不會更改,因此不可能發生衝突,並且迭代器保證不會拋出 ConcurrentModificationException。創建迭代器以後,迭代器就不會反映列表的添加、移除或者更改。在迭代器上進行的元素更改操作(remove、set 和 add)不受支持。這些方法將拋出 UnsupportedOperationException。 允許使用所有元素,包括 null。

內存一致性效果:當存在其他併發 collection 時,將對象放入 CopyOnWriteArrayList 之前的線程中的操作 happen-before 隨後通過另一線程從 CopyOnWriteArrayList 中訪問或移除該元素的操作。


以上介紹摘自API文檔。

根據API對CopyOnWriteArrayList的介紹,其原理以及使用場景已經比較清晰了,下面我們通過源碼來分析下。

實現原理

API已經說的比較清楚了,由於數組的特殊結構,所以如果想要對數據進行結構性修改,如增加一個元素,刪除一個元素,都是很麻煩的,所以無法將對一個數組的結構性修改縮小到一個原子指令範圍,不像鏈表可以通過CAS修改next指針來修改鏈表。所以CopyOnWriteArrayList通過將任何對底層數組進行結構性修改的操作變成針對一個新的副本的修改,然後用修改後的副本來替換原來的數組,來實現遍歷與修改分離,以保證數組高效的訪問效率。

常用方法解讀

CopyOnWriteArrayList的重要的幾個方法:add(int, E)/add(E)/set(int, E)/remove(int)/iterator(),其中前四個是對CopyOnWriteArrayList的結構進行修改,最後一個是對CopyOnWriteArrayList進行遍歷。下面針對源碼逐一進行分析。

add(int, E)和add(E)
/**
* Inserts the specified element at the specified position in this list.
 * Shifts the element currently at that position (if any) and any subsequent
 * elements to the right (adds one to their indices).
 *
 * @throws IndexOutOfBoundsException
 *             {@inheritDoc}
 */
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表示插入的位置是數組的最後一個位置,所以該位置前面的元素原樣不動複製到新的數組即可
            // 這裏通過複製elements數組生成一個新的數組,注意這裏新的數組長度是原數組+1,所以新數組的最後一個元素是NULL
            newElements = Arrays.copyOf(elements, len + 1);
        else {
            // 將原數組的0~index-1原樣複製到新的數組中,
            // 而index之後的元素對應複製到新數組的index+1之後,即中間空出一個位置用於放置帶插入元素
            newElements = new Object[len + 1];
            System.arraycopy(elements, 0, newElements, 0, index);
            System.arraycopy(elements, index, newElements, index + 1, numMoved);
        }
        // 將element插入到新的數組
        newElements[index] = element;
        // 將更新底層數組的引用,由於array是volatile的,所以對其的修改能夠立即被後續線程可見
        setArray(newElements);
    } finally {
        // 釋放鎖
        lock.unlock();
    }
}

/**
* Appends the specified element to the end of this list.
 *
 * @param e
 *            element to be appended to this list
 * @return <tt>true</tt> (as specified by {@link Collection#add})
 */
// @By Vicky:該方法相當於調用add(array.length, e)
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()方法的實現很簡單,通過加鎖保證線程安全,通過Arrays.copyOf根據原數組複製一個新的數組,將要插入的元素插入到新的數組的對應位置,然後將新的數組賦值給array,通過volatile保證內存可見。

set(int, E)
/**
 * Replaces the element at the specified position in this list with the
 * specified element.
 *
 * @throws IndexOutOfBoundsException
 *             {@inheritDoc}
 */
// @By Vicky:更新指定位置元素
public E set(int index, E element) {
    // 加鎖
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
        Object[] elements = getArray();
        // 獲取需更新的元素
        Object oldValue = elements[index];
        // //
        // 需更新的值不等於原值(注意此處的不等是==,不是equals(),即oldValue和element必須是引用同一個對象纔可)
        if (oldValue != element) {
            int len = elements.length;
            // 複製一個新的數組,並將index更新成新的值,更新引用
            Object[] newElements = Arrays.copyOf(elements, len);
            newElements[index] = element;
            setArray(newElements);
        } else {
            // Not quite a no-op; ensures volatile write semantics
            // 此處由於更新的值與原值是同一個對象,所以其實可不更新引用
            // 從註釋可以看出更新的目的是出於寫volatile變量
            setArray(elements);
        }
        return (E) oldValue;
    } finally {
        // 釋放鎖
        lock.unlock();
    }
}

set()比add()更新簡單,只需要複製一個新的數組,然後更新新的數組的指定位置的元素,然後更新引用即可。

remove(int)
/**
   * Removes the element at the specified position in this list.
    * Shifts any subsequent elements to the left (subtracts one from their
    * indices).  Returns the element that was removed from the list.
    *
    * @throws IndexOutOfBoundsException {@inheritDoc}
    */
// @By Vicky:刪除指定位置的元素
public E remove(int index) {
    // 加鎖
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
        Object[] elements = getArray();
        int len = elements.length;
        Object oldValue = elements[index];
        int numMoved = len - index - 1;// 需要移動的元素的個數
        if (numMoved == 0) // ==0表示刪除的位置是數組的最後一個元素,只需要簡單的複製原數組的len-1個元素到新數組即可
            setArray(Arrays.copyOf(elements, len - 1));
        else {
            // 將原數組的0-index-1複製到新數組的對應位置
            // 將原數組的index+1之後的元素複製到新數組,丟棄原數組的index位置的元素
            Object[] newElements = new Object[len - 1];
            System.arraycopy(elements, 0, newElements, 0, index);
            System.arraycopy(elements, index + 1, newElements, index, numMoved);
            setArray(newElements);
        }
        return (E) oldValue;
    } finally {
        lock.unlock();
    }
}

// @By Vicky:刪除指定元素,而非指定位置的元素
public boolean remove(Object o) {
    // 加鎖
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
        Object[] elements = getArray();
        int len = elements.length;
        if (len != 0) {
            // Copy while searching for element to remove
            // This wins in the normal case of element being present
            int newlen = len - 1;// 刪除之後數組的長度
            Object[] newElements = new Object[newlen];// 創建新的數組

            for (int i = 0; i < newlen; ++i) {// 從0-len-1遍歷原數組
                if (eq(o, elements[i])) {// 如果是待刪除元素,則將該元素之後的元素複製到新數組中
                    // found one; copy remaining and exit
                    for (int k = i + 1; k < len; ++k)
                        newElements[k - 1] = elements[k];
                    // 設置新數組
                    setArray(newElements);
                    return true;
                } else
                    // 將該元素插入到新數組
                    newElements[i] = elements[i];
            }

            // 確認最後原數組一個元素是否與待刪除元素相等,是的話直接將修改引用即可,因爲前面已經爲新數組賦完值了
            // special handling for last cell
            if (eq(o, elements[newlen])) {
                setArray(newElements);
                return true;
            }
        }
        // 到這裏說明數組中沒有與待刪除元素相等的元素,所以直接返回false,
        // 但是這裏並沒有寫volatile變量,看來set那裏也只是寫着好玩
        return false;
    } finally {
        lock.unlock();
    }
}

remove()有兩種方式,根據指定位置刪除以及指定元素刪除兩種方式。

iterator()

這裏的iterator()只是很簡單的迭代器,內部將remove/set/add三個修改操作進行了限制,因爲這裏的迭代器不能修改集合,代碼就不細看了。注意到iterator並沒有加鎖,因爲iterator所訪問的數組是不會變的,就算有其他線程對集合進行修改。

使用場景

CopyOnWriteArrayList適合讀多寫少的場景。通過空間換時間的方式來提高讀的效率並保證寫的安全性。


歡迎訪問我的個人博客,尋找更多樂趣~

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