CopyOnWriteArrayList原始碼閱讀筆記

三分惡發表於2020-08-17

簡介

ArrayList是開發中使用比較多的集合,它不是執行緒安全的,CopyOnWriteArrayList就是執行緒安全版本的ArrayList。CopyOnWriteArrayList同樣是通過陣列實現,這個類的名字叫“CopyOnWrite ”,它是在寫入的時候拷貝陣列,對副本進行操作。


原理

CopyOnWriteArrayList採用了一種讀寫分離的併發策略。CopyOnWriteArrayList容器允許併發讀,讀操作是無鎖的,效能較高。至於寫操作,比如向容器中新增一個元素,則首先將當前容器複製一份,然後在新副本上執行寫操作,結束之後再將原容器的引用指向新容器。示意圖如下:

在這裡插入圖片描述



繼承體系

在這裡插入圖片描述
通過類圖,可以看到CopyOnWriteArrayList的繼承體系·:

  • 實現了List, RandomAccess, Cloneable, java.io.Serializable等介面。

  • 實現了List,提供了基礎的新增、刪除、遍歷等操作。

  • 實現了RandomAccess,提供了隨機訪問的能力。

  • 實現了Cloneable,可以被克隆。

  • 實現了Serializable,可以被序列化。


原始碼分析

屬性

    //可重入鎖,保證執行緒安全
    final transient ReentrantLock lock = new ReentrantLock();
    
    //存放資料元素的陣列,只能通過get/set方法訪問
    private transient volatile Object[] array;

    final Object[] getArray() {
        return array;
    }
    
    final void setArray(Object[] a) {
        array = a;
    }
  • lock:用於修改時加鎖,使用transient修飾表示不自動序列化。
  • array:被使用volatile修飾表示一個執行緒對這個欄位的修改另外一個執行緒立即可見。

構造方法

  • 無參構造方法:建立一個空陣列
public CopyOnWriteArrayList() {
        setArray(new Object[0]);
 }
  • 有參構造方法,引數為集合
    public CopyOnWriteArrayList(Collection<? extends E> c) {
        Object[] elements;
         // 如果c也是CopyOnWriteArrayList型別
        // 那麼直接把它的陣列拿過來使用
        if (c.getClass() == CopyOnWriteArrayList.class)
            elements = ((CopyOnWriteArrayList<?>)c).getArray();
        else {
           //否則,先轉換為陣列
            elements = c.toArray();
            // c.toArray might (incorrectly) not return Object[] (see 6260652)
           //  檢查c.toArray()返回的是不是Object[]型別,如果不是,重新拷貝成Object[].class型別
            if (elements.getClass() != Object[].class)
                elements = Arrays.copyOf(elements, elements.length, Object[].class);
        }
        setArray(elements);
    }
  • 有參構造方法,引數為陣列
    //把toCopyIn的元素拷貝給當前list的陣列。
    public CopyOnWriteArrayList(E[] toCopyIn) {
        setArray(Arrays.copyOf(toCopyIn, toCopyIn.length, Object[].class));
    }

add(E 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(int index, E element)

在指定位置插入陣列

 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)
            // 如果插入的位置是最後一位
            // 那麼拷貝一個n+1的陣列, 其前n個元素與舊陣列一致
                newElements = Arrays.copyOf(elements, len + 1);
            else {
                // 如果插入的位置不是最後一位
               // 那麼新建一個n+1的陣列
                newElements = new Object[len + 1];
                //拷貝舊陣列[0,……index-1]下標的元素
                System.arraycopy(elements, 0, newElements, 0, index);
                //拷貝舊陣列的其餘元素到新陣列[index+1,……length+1],剛好空出了index下標位置
                System.arraycopy(elements, index, newElements, index + 1,
                                 numMoved);
            }
            //將插入的元素放到index下標位置
            newElements[index] = element;
            //給array賦值
            setArray(newElements);
        } finally {
           //釋放鎖
            lock.unlock();
        }
    }

寫入操作:

  • 在上面新增元素的操作中,都進行了加鎖的操作
  • 拷貝一個新陣列,長度等於原陣列長度加1,並把原陣列元素拷貝到新陣列中
  • 把新陣列賦值給當前物件的array屬性,覆蓋原陣列

remove(int index)

根據下標位置移除資料元素:

    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)
            // 如果移除的是最後一位
            // 那麼直接拷貝一份n-1的新陣列, 最後一位就自動刪除了
                setArray(Arrays.copyOf(elements, len - 1));
            else {
              // 如果移除的不是最後一位
             // 那麼新建一個n-1的新陣列
                Object[] newElements = new Object[len - 1];
                // 將前index個元素拷貝到新陣列中
                System.arraycopy(elements, 0, newElements, 0, index);
                // 將index後面(不包含)的元素往前挪一位
               // 這樣正好把index位置覆蓋掉了, 相當於刪除了
                System.arraycopy(elements, index + 1, newElements, index,
                                 numMoved);
                setArray(newElements);
            }
            return oldValue;
        } finally {
            //釋放鎖
            lock.unlock();
        }
    }

刪除操作:刪除操作同理,將除要刪除元素之外的其他元素拷貝到新副本中,然後切換引用,將原容器引用指向新副本。同屬寫操作,需要加鎖。


get(int index)

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

獲取操作:獲取操作屬於讀操作,直接通過陣列下標獲取資料元素,沒有加鎖,所以保證了效能。


size()

    public int size() {
       //返回陣列長度
        return getArray().length;
    }

和ArrayList不同,檢視ArrayList原始碼閱讀筆記,可以發現ArrayList中是有size屬性的,這是因為ArrayList陣列的長度實際是要大於集合的大小的。CopyOnWriteArrayList每次修改都是拷貝一份正好可以儲存目標個數元素的陣列,所以不需要size屬性,直接返回陣列長度即可。


總結

  • CopyOnWriteArrayList使用ReentrantLock重入鎖加鎖,保證執行緒安全;

  • CopyOnWriteArrayList的寫操作都要先拷貝一份新陣列,在新陣列中做修改,修改完了再用新陣列替換老陣列,所以空間複雜度是O(n),效能相對低下;

  • CopyOnWriteArrayList的讀操作支援隨機訪問,時間複雜度為O(1);

  • CopyOnWriteArrayList採用讀寫分離的思想,讀操作不加鎖,寫操作加鎖,且寫操作佔用較大記憶體空間,所以適用於讀多寫少的場合;

  • CopyOnWriteArrayList只保證最終一致性,不保證實時一致性;




紙上得來終覺淺,絕知此事要躬行。



參考:

【1】:【死磕 Java 集合】— CopyOnWriteArrayList原始碼分析
【2】:CopyOnWriteArrayList實現原理及原始碼分析

相關文章