Java集合乾貨——CopyOnWriteArrayList原始碼分析

冰洋發表於2019-02-24

前言

CopyOnWriteArrayList是一個執行緒安全集合,原理簡單說就是:在保證執行緒安全的前提下,犧牲掉寫操作的效率來保證讀操作的高效。所謂CopyOnWrite就是通過複製的方式來完成對資料的修改,在進行修改的時候,複製一個新陣列,在新陣列上面進行修改操作,這樣就保證了不改變老陣列,也就沒有一寫多讀資料不一致的問題了。

具體的實現來看原始碼,JDK 8。

CopyOnWriteArrayList

定義

public class CopyOnWriteArrayList<E>
    implements List<E>, RandomAccess, Cloneable, java.io.Serializable 
複製程式碼

在定義上和ArrayList大差不差,不過多解釋,有興趣可以看之前關於ArrayList的文章。

屬性

一個是Lock,另一個是一個物件陣列。

/** The lock protecting all mutators */
//一把鎖
transient final ReentrantLock lock = new ReentrantLock();

/** The array, accessed only via getArray/setArray. */
//一個物件陣列,只從方法getArray/setArray處接受值
//volatile後面會有專門的文章來說明
private volatile transient Object[] array;
複製程式碼

初始化

CopyOnWriteArrayList的初始化容量是0,分為這樣的幾個步驟。

//在無參構造方法中會呼叫setArray方法,引數是一個空的物件陣列,然後通過setArray把這個空的陣列賦值給屬性array
public CopyOnWriteArrayList() {
  setArray(new Object[0]);
}
final void setArray(Object[] a) {
  array = a;
}
複製程式碼

需要說明的是另一個有參構造方法,引數可以是一個集合

//按照集合的迭代器返回的順序建立一個包含指定集合元素的列表
public CopyOnWriteArrayList(Collection<? extends E> c) {
  //將集合轉為陣列
  Object[] elements = c.toArray();
//elements不能夠是一個空的物件陣列 為什麼要if這樣一個條件嘞  因為屬性中需要賦值的是一個物件陣列  所以如果if成立執行的就是把原陣列變為一個物件陣列  如果本身就是物件陣列也就不用轉了
  if (elements.getClass() != Object[].class)
    elements = Arrays.copyOf(elements, elements.length, Object[].class);
  //賦值給屬性
  setArray(elements);
}
複製程式碼

方法

add(E e)

新增一個新元素到list的尾部。

public boolean add(E e) {
  //鎖 1.5新版本的鎖 已經不用synchronized了
  final ReentrantLock lock = this.lock;
  //加鎖
  lock.lock();
  try {
    //getArray獲取屬性值 就是老陣列
    Object[] elements = getArray();
    int len = elements.length;
    //這裡是重點 在這裡 複製老陣列得到了一個長度+1的新陣列
    Object[] newElements = Arrays.copyOf(elements, len + 1);
    //新增元素
    newElements[len] = e;
    //用新陣列取代老陣列
    setArray(newElements);
    return true;
  } finally {
    lock.unlock();
  }
}
複製程式碼

從add方法中我們可以看到所謂的CopyOnWrite是如何實現的,在需要修改的時候,複製一個新陣列,在新陣列上修改,修改結束取代老陣列,這樣保證了修改操作不影響老陣列的正常讀取,另修改操作是加鎖的,也就是說沒有了執行緒不安全的問題。

和ArrayList相比較,效率比較低,只新增一個元素的情況下(初始容量均為0),用時是ArrayList的5倍左右,但是隨著CopyOnWriteArrayList中元素的增加,CopyOnWriteArrayList的修改代價將越來越昂貴。

除了新增其他的修改操作也都是這樣的套路,不做過多解釋,如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();
  }
}
複製程式碼

#####get

public E get(int index) {
  return get(getArray(), index);
}
//按照下標獲取陣列中對應的元素
private E get(Object[] a, int index) {
  return (E) a[index];
}
複製程式碼

讀取的方法就很簡單了,按照下標獲取對應的元素。

CopyOnWriteArrayList總結
  1. 讀寫分離,我們修改的是新陣列,讀取的是老陣列,不是一個物件,實現了讀寫分離。這種技術資料庫用的非常多,在高併發下為了緩解資料庫的壓力,即使做了快取也要對資料庫做讀寫分離,讀的時候使用讀庫,寫的時候使用寫庫,然後讀庫、寫庫之間進行一定的同步,這樣就避免同一個庫上讀、寫的IO操作太多。
  2. 場景:讀操作遠多於修改操作

我不能保證每一個地方都是對的,但是可以保證每一句話,每一行程式碼都是經過推敲和斟酌的。希望每一篇文章背後都是自己追求純粹技術人生的態度。

永遠相信美好的事情即將發生。

相關文章