1. 程式人生 > 其它 >併發和多執行緒(十七)--CopyOnWriteArrayList原始碼解析

併發和多執行緒(十七)--CopyOnWriteArrayList原始碼解析

目錄
我們肯定都使用過ArrayList,但是多執行緒或併發環境下,ArrayList作為共享變數被訪問,是執行緒不安全的。我們可以選擇自己加鎖或Collections.synchronizedList()去實現一個執行緒安全的容器。除此之外,我們還可以使用今天要學習的CopyOnWriteArrayList。

CopyOnWrite:

是一種策略,表示在使用的時候,將共享內容拷貝一份進行修改,修改完成之後,然後將原容器指向修改後的內容,保證的是最終一致性,因為讀寫是不同的容器,讀的時候不加鎖,寫的時候加鎖。JDK中有CopyOnWriteArrayList和CopyOnWriteArraySet兩種已經實現的容器,瞭解這種思想,我們甚至可以自定義實現別的容器,例如Map,但是為什麼沒有實現CopyOnWriteHashMap呢,因為已經有了優秀的ConcurrentHashMap。

類註釋:

從類註釋我們可以得到以下資訊:

  1. CopyOnWriteArrayList是ArrayList的安全變體。
  2. CopyOnWriteArrayList的實現需要陣列拷貝有一定的成本,但是通過比其他的方案效率更高。
  3. 允許存放各種元素,包括null。
  4. 迭代過程中,進行修改不會出現ConcurrentModificationException,因為讀寫分離。

類屬性:

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

    final transient ReentrantLock lock = new ReentrantLock();

    private transient volatile Object[] array;

    final Object[] getArray() {
        return array;
    }

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

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

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

    public CopyOnWriteArrayList(E[] toCopyIn) {
        setArray(Arrays.copyOf(toCopyIn, toCopyIn.length, Object[].class));
    }
}

從上面的程式碼可以得到以下資訊:

  1. CopyOnWriteArrayList有一個ReentrantLock實現的鎖。
  2. 和ArrayList一樣,都是通過陣列儲存資料,但是多個volatile的修飾,用來滿足Happen-Before原則。
  3. 預設建構函式,將陣列初始化為1,沒有設定陣列大小的建構函式,只能講陣列或者容器傳入。

新增

    public void add(int index, E element) {
        final ReentrantLock lock = this.lock;
        //通過ReentrantLock加鎖
        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;
            //如果直接插入到尾部,直接使用copyOf複製一個新的陣列,length+1
            if (numMoved == 0)
                newElements = Arrays.copyOf(elements, len + 1);
            else {
                //直接初始化一個新的陣列,長度+1
                newElements = new Object[len + 1];
                //複製index前面的部分到新陣列
                System.arraycopy(elements, 0, newElements, 0, index);
                //複製index後面的部分到新陣列的相應位置
                System.arraycopy(elements, index, newElements, index + 1,
                                 numMoved);
            }
            //對index位置賦值
            newElements[index] = element;
            //設定到陣列中
            setArray(newElements);
        } finally {
            //解鎖
            lock.unlock();
        }
    }

這裡選擇了插入對應下標的方法,如果是插入到尾部,更加簡單,可以自行檢視。和ArrayList的相關原始碼差不多,除了加鎖解鎖,就是ArrayList是直接對原陣列進行修改,而CopyOnWriteArrayList選擇生成一個新的陣列,先將elements的index前面部分複製過去,然後複製後面的部分,然後將element賦值,整體程式碼非常簡單明瞭。

System.arraycopy()

引數分別為:原陣列,複製的起始下標,新陣列,複製到新陣列原則的起始下標,複製的長度。

獲取

	public E get(int index) {
        return get(getArray(), index);
    }
	private E get(Object[] a, int index) {
        return (E) a[index];
    }

get()沒啥好講的,就是從陣列中獲取,可以看到沒有通過鎖進行加密,讀的時候不進行加鎖,很有可能出現讀到舊資料的情況。

刪除

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

步驟如下

  1. 加鎖。
  2. 通過下標獲取oldValue。
  3. 如果index為最後一位,直接複製資料,長度-1.
  4. 如果index不是最後一位,生成一個新陣列,然後先複製index前面的部分,然後index後面的資料整體遷移一位。
  5. 解鎖。

修改

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

步驟如下

  1. 加鎖。
  2. 通過引數下標獲取oldValue。
  3. 如果oldValue和引數element不等,生成一個新陣列,然後直接替換index位置的資料,setArray()。
  4. 否則setArray(),這一步沒看太懂,看了上面的註釋,確保volatile的寫語義。
  5. 返回oldValue,解鎖。

批量刪除

    public boolean removeAll(Collection<?> c) {
        if (c == null) throw new NullPointerException();
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            Object[] elements = getArray();
            int len = elements.length;
            if (len != 0) {
                int newlen = 0;
                Object[] temp = new Object[len];
                for (int i = 0; i < len; ++i) {
                    Object element = elements[i];
                    if (!c.contains(element))
                        temp[newlen++] = element;
                }
                if (newlen != len) {
                    setArray(Arrays.copyOf(temp, newlen));
                    return true;
                }
            }
            return false;
        } finally {
            lock.unlock();
        }
    }

步驟如下

  1. 加鎖。
  2. 當前陣列為空,返回false,否則繼續。
  3. 定義變數newlen,和一個長度為len的空陣列,遍歷elements陣列,判斷要刪除的collection中是否當前下標值,如果不包含儲存到temp陣列。
  4. 如果newlen != len,複製一個長度為newlen的陣列,setArray(),佛足額返回false。
  5. 解鎖。

從上面我們看到批量刪除並不是遍歷然後刪除每個元素,因為刪除元素每次都會有一次陣列拷貝,這樣會很消耗效能,加鎖時間變得很長,影響併發,所以這是批量刪除更好的思路,將不需要刪除的元素都放到新陣列中,而不是挨個刪除。

關於迭代:

CopyOnWriteArrayList通過COWIterator實現的迭代器,每次迭代,持有的是原陣列的引用,所以修改和新增不會影響到迭代。

總結:

通過上面的原始碼,我們隊CopyOnWriteArrayList有了基本的瞭解,由於讀的時候不加鎖,而synchronizedList讀寫都是加鎖的,所以相對來說還是比較推薦使用前者,在併發多執行緒的場景下。