JUC---CopyOnWriteArrayList源碼解析(JDK13)

java.util.concurrent包系列文章
JUC—ThreadLocal源碼解析(JDK13)
JUC—ThreadPoolExecutor線程池源碼解析(JDK13)
JUC—各種鎖(JDK13)
JUC—原子類Atomic*.java源碼解析(JDK13)
JUC—CAS源碼解析(JDK13)
JUC—ConcurrentHashMap源碼解析(JDK13)
JUC—CopyOnWriteArrayList源碼解析(JDK13)
JUC—併發隊列源碼解析(JDK13)
JUC—多線程下控制併發流程(JDK13)
JUC—AbstractQueuedSynchronizer解析(JDK13)


一、CopyOnWrite*系列

CopyOnWriteArrayList和CopyOnWriteArraySet​也是常用的併發集合類。​他們支持併發的讀寫​。線程安全的。不過有它的缺點。
本篇就分析下CopyOnWriteArrayList的原理和源碼。CopyOnWriteArraySet​也是類似的。


二 、CopyOnWriteArrayList

CopyOnWriteArrayList誕生自JDK1.5,是爲了取代Vector和SynchronizedList的,就和ConcurrentHashMap取代SynchronizedMap一樣。Vector和SynchronizedList鎖的粒度太大,併發效率低。並且迭代時無法編輯。

適用場景:

  • 多讀少寫的情況,比如系統緩存的白名單。偶爾更新一次,讀卻特別多。

讀寫規則:

  • 讀取完全不用加鎖,寫入也不會阻塞讀操作。只有寫入與寫入之間需要同步等待。

看一份代碼:

public static void main(String[] args) {
    CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();
    list.add("A");
    list.add("B");
    list.add("C");
    list.add("D");
    // CopyOnWriteArrayList生成迭代器。迭代器裏的數組就是生成迭代器時list的數組。
    // 後續的對list的修改時對這個數組的數據不產生影響。修改是在新的內存中新copy了一個同樣的數組。
    // 修改完成後再把list數組指向這個新數組。
    Iterator<String> iterator = list.iterator();
    while (iterator.hasNext()) {
        System.out.println("list is " + list);
        String next = iterator.next();
        System.out.println("next is " + next);
        if (next.equals("B")) {
            list.remove("D");
        }
        if (next.equals("C")) {
            list.add("E");
        }
    }
}
運行結果:
list is [A, B, C, D]
next is A
list is [A, B, C, D]
next is B
list is [A, B, C]
next is C
list is [A, B, C, E]
next is D

這就是CopyOnWrite的意思。創建新副本,讀寫分離。add的時候會在新內存copy一份原數組,add完成之後讓CopyOnWriteArrayList的array指向新數組,舊的就會被回收了。實現了讀寫不需要同步。寫與寫才需要同步。
上面把CopyOnWriteArrayList換成ArrayList是不行的,ArrayList不支持在迭代時操作數組。

ArrayList報錯原因:
看看ArrayList裏面迭代器的屬性和方法

 public E next() {
 	// 獲取下一個元素之前會判斷當前數組長度是否變化了
    checkForComodification();
    int i = cursor;
    if (i >= size)
        throw new NoSuchElementException();
    Object[] elementData = ArrayList.this.elementData;
    if (i >= elementData.length)
        throw new ConcurrentModificationException();
    cursor = i + 1;
    return (E) elementData[lastRet = i];
}

// expectedModCount是生成迭代器時modCount賦給expectedModCount
int expectedModCount = modCount;

final void checkForComodification() {
	// modCount每次修改就+1,
	// expectedModCount是生成迭代器時modCount賦給expectedModCount
	if (modCount != expectedModCount)
		// 如果數組長度變化了,說明被修改了,直接報錯
        throw new ConcurrentModificationException();
}

CopyOnWriteArrayList的add方法​

public boolean add(E e) {
	// 上鎖,寫必須同步
   synchronized (lock) {
       Object[] es = getArray();
       int len = es.length;
       // 複製了一個新數組,並且長度是原來的+1
       es = Arrays.copyOf(es, len + 1);
       // 新加的元素添加到數組最後
       es[len] = e;
       // add完成後,把array指向新數組
       setArray(es);
       return true;
   }
}final void setArray(Object[] a) {
    array = a;
}

CopyOnWriteArrayList的get方法​
可以發現get方法沒有任何的加鎖行爲

public E get(int index) {
    return elementAt(getArray(), index);
}

static <E> E elementAt(Object[] a, int index) {
    return (E) a[index];
}

初始化方法與鎖

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

    private transient volatile Object[] array;

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

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

CopyOnWriteArrayList的缺點:

  • 迭代期間的數據可能過時了的
  • 內存佔用更大,因爲是copy一份數組嘛

  • 我的公衆號:Coding摳腚
  • 偶爾發發自己最近學到的乾貨。學習路線,經驗,技術分享。技術問題交流探討。
    Coding摳腚
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章