淺析CopyOnWriteArrayList

CopyOnWriteArrayList引入

模擬傳統的ArrayList出現執行緒不安全的現象

public class Demo1 {
    public static void main(String[] args) {
        //List<String> list = new CopyOnWriteArrayList<>();
        List<String> list = new ArrayList<>();

        //開啟50個執行緒往ArrayList中添加數據
        for (int i = 1; i <= 50; i++) {
            new Thread(() -> {
                list.add(UUID.randomUUID().toString().substring(0, 5));
                System.out.println(list);
            }, String.valueOf(i)).start();
        }

    }
}

運行結果如下:由於fail-fast機制的存在,拋出了modcount修改異常的錯誤(modcount是ArrayList源碼中的一個變數,用來表示修改的次數,因為ArrayList不是為並發情況而設計的集合類)


如何解決該問題呢?

方式一:可以使用Vector集合,Vector集合是執行緒安全版的ArrayList,其方法都上了一層synchronized進行修飾,採取jvm內置鎖來保證其並發情況下的原子性、可見性、有序性。但同時也帶來了性能問題,因為synchronized一旦膨脹到重量級鎖,存在用戶態到和心態的一個轉變,多執行緒的上下文切換會帶來開銷。另一個問題是Vector集合的擴容沒有ArrayList的策略好

List<String> list = new Vector<>();

方式二:使用Collections.synchronizedList

List<String> list = Collections.synchronizedList(new ArrayList<>());

方式三:採用JUC提供的並發容器,CopyOnWriteArrayList

List<String> list = new CopyOnWriteArrayList<>();

CopyOnWriteArrayList淺析

和ArrayList一樣,其底層數據結構也是數組,加上transient不讓其被序列化,加上volatile修飾來保證多執行緒下的其可見性和有序性

先來看看其構造函數是怎麼一回事

    public CopyOnWriteArrayList() {
       //默認創建一個大小為0的數組
        setArray(new Object[0]);
    }

    final void setArray(Object[] a) {
        array = a;
    }
	
    public CopyOnWriteArrayList(Collection<? extends E> c) {
        Object[] elements;
        //如果當前集合是CopyOnWriteArrayList的類型的話,直接賦值給它
        if (c.getClass() == CopyOnWriteArrayList.class)
            elements = ((CopyOnWriteArrayList<?>)c).getArray();
        else {
         	//否則調用toArra()將其轉為數組   
            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));
    }
	

在來看看其讀數據的幾個操作,可見都沒上鎖,這就奇怪了,那如何去保證執行緒安全呢?

    final Object[] getArray() {
        return array;
    }
    public int size() {
        return getArray().length;
    }
   public boolean isEmpty() {
        return size() == 0;
    }
    public int indexOf(E e, int index) {
        Object[] elements = getArray();
        return indexOf(e, elements, index, elements.length);
    }
    public int lastIndexOf(Object o) {
        Object[] elements = getArray();
        return lastIndexOf(o, elements, elements.length - 1);
    }
	
	........

在來看看其修改時的add函數

    public boolean add(E e) {
        //使用ReentrantLock上鎖
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            //調用getArray()獲取原來的數組
            Object[] elements = getArray();
            int len = elements.length;
            //複製老數組,得到一個長度+1的數組
            Object[] newElements = Arrays.copyOf(elements, len + 1);
            //添加元素,在用setArray()函數替換原數組
            newElements[len] = e;
            setArray(newElements);
            return true;
        } finally {
            lock.unlock();
        }
    }

可見其修改操作是基於fail-safe機制,像我們的String一樣,不在原來的對象上直接進行操作,而是複製一份對其進行修改,另外此處的修改操作是利用Lock鎖進行上鎖的,所以保證了執行緒安全問題。

在來看看remove操作,看是不是如此做的

    public boolean remove(Object o) {
        Object[] snapshot = getArray();
        int index = indexOf(o, snapshot, 0, snapshot.length);
        return (index < 0) ? false : remove(o, snapshot, index);
    }

    private boolean remove(Object o, Object[] snapshot, int index) {
        final ReentrantLock lock = this.lock;
        //上鎖
        lock.lock();
        try {
            Object[] current = getArray();
            int len = current.length;
            if (snapshot != current) findIndex: {
                int prefix = Math.min(index, len);
                for (int i = 0; i < prefix; i++) {
                    if (current[i] != snapshot[i] && eq(o, current[i])) {
                        index = i;
                        break findIndex;
                    }
                }
                if (index >= len)
                    return false;
                if (current[index] == o)
                    break findIndex;
                index = indexOf(o, current, index, len);
                if (index < 0)
                    return false;
            }
            //複製一個數組
            Object[] newElements = new Object[len - 1];
            System.arraycopy(current, 0, newElements, 0, index);
            System.arraycopy(current, index + 1,
                             newElements, index,
                             len - index - 1);
            //替換原數組
            setArray(newElements);
            return true;
        } finally {
            lock.unlock();
        }
    }

可見其思路是一致的,我們在與ArrayList去對比一下,可見其效率比ArrayList低不少,畢竟多執行緒場景下,其每次都是要在原數組基礎上複製一份在操作耗記憶體和時間,而ArrayList只是容量滿了進行擴容,因此在非多執行緒的場景下還是用ArrayList吧。

這也解決了我之前的疑問,為啥還學ArrayList呢,JUC版的CopyOnWriteArrayList可以干ArrayList幹不了的事,咱們直接用CopyOnWriteArrayList不也挺香。

小結

  • CopyOnWriteArrayList適合於多執行緒場景下使用,其採用讀寫分離的思想,讀操作不上鎖,寫操作上鎖,且寫操作效率較低
  • CopyOnWriteArrayList基於fail-safe機制,每次修改都會在原先基礎上複製一份,修改完畢後在進行替換
  • CopyOnWriteArrayList採用的是ReentrantLock進行上鎖。
Tags: