面試官:CopyOnWriteArrayList是如何保證線程安全的?

互聯網高級架構師 發佈 2022-09-16T11:53:40.050661+00:00

在我們需要保證線程安全的時候,如果使用到Map,那麼我們可以使用線程安全的ConcurrentHashMap,ConcurrentHashMap不僅可以保證線程安全,而且效率也非常不錯,那有沒有線程安全的List呢?答案是有,那就是CopyOnWriteArrayList。

在我們需要保證線程安全的時候,如果使用到Map,那麼我們可以使用線程安全的ConcurrentHashMap,ConcurrentHashMap不僅可以保證線程安全,而且效率也非常不錯,那有沒有線程安全的List呢?答案是有,那就是CopyOnWriteArrayList。今天我們就一起來了解一下CopyOnWriteArrayList,看它是如何巧妙的保證線程安全的吧。

一:成員變量分析

    //進行修改操作時的鎖
    final transient Reentrantlock lock = new ReentrantLock();

    //真正保存數據的數組 用volatile關鍵字進行修飾,保證array的引用的可見性
    private transient volatile Object[] array;

二:源碼分析

首先我們看構造方法,CopyOnWriteArrayList有三個構造方法。

1.空參構造

調用setArray方法將成員變量array賦值為一個長度為0的數組。

public CopyOnWriteArrayList() {
        setArray(new Object[0]);
    }
final void setArray(Object[] a) {
        array = a;
    }

2.傳入一個Collection對象的構造方法

首先判斷Collection是否是一個CopyOnWriteArrayList,如果是,直接將傳入的CopyOnWriteArrayList的elements重新賦值給需要創建的CopyOnWriteArrayList。 如果不是,判斷Collection是否是ArrayList,如果是,那麼就利用toArray()方法將其轉化為一個數組並賦值給成員變量array,否則將Collection裡面的元素全部取出來copy到一個新數組中,並且將該數組賦值給成員變量array。

    public CopyOnWriteArrayList(Collection<? extends E> c) {
        Object[] elements;
        if (c.getClass() == CopyOnWriteArrayList.class)
            elements = ((CopyOnWriteArrayList<?>)c).getArray();
        else {
            elements = c.toArray();
            if (c.getClass() != ArrayList.class)
                elements = Arrays.copyOf(elements, elements.length, Object[].class);
        }
        setArray(elements);
    }

3.傳入一個數組的構造方法

將傳入的數組的元素copy到一個新的Object數組,並且賦值給成員變量array。

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

接下來我們看核心的add(),remove(),get()方法。

add(E e)

首先加鎖,然後通過Arrays.copyOf()方法將元素copy到一個新的數組中,新的數組的長度為原數組的長度+1,並且將需要加入的元素賦值到新數組的最後。最後將新數組賦值給成員變量array。

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)

add(int index, E element)方法需要將元素加入到指定的索引位置中。首先也是先加鎖,保證線程安全,將原數組分為兩段進行操作,根據index進行分隔,分別copy index之前的元素和之後的元素,copy完成之後在將需要插入的元素設置到索引為index的位置上。之後將新數組賦值給成員變量array。

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)
                newElements = Arrays.copyOf(elements, len + 1);
            else {
                newElements = new Object[len + 1];
                System.arraycopy(elements, 0, newElements, 0, index);
                System.arraycopy(elements, index, newElements, index + 1,
                                 numMoved);
            }
            newElements[index] = element;
            setArray(newElements);
        } finally {
            lock.unlock();
        }
    }

接下來看remove()方法。

remove(int index)

remove(int index)方法需要在數組中移除指定索引的值。首先是加鎖,同樣也是將原數組分為兩段進行操作,根據index進行分隔,分別copy index之前的元素和之後的元素,copy到一個新數組中,新數組的長度為原數組的長度減一(注意這裡是沒有copy index索引位置的值的,所以相當於移除了index索引上的值)。之後將新數組賦值給成員變量array。

    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()方法

get()

我們可以看到get()方法很簡單,就是從array成員變量中取出對應索引的值。並沒有加鎖處理。所以儘管是在並發高的情況下,get()方法的效率依舊是比較高的。

    /**
     * {@inheritDoc}
     *
     * @throws IndexOutOfBoundsException {@inheritDoc}
     */
    public E get(int index) {
        return get(getArray(), index);
    }
    
    private E get(Object[] a, int index) {
        return (E) a[index];
    }

三:總結

CopyOnWriteArrayList為什麼能夠保證線程安全,主要是因為以下幾點:

  1. 在做修改操作的時候加鎖
  2. 每次修改都是將元素copy到一個新的數組中,並且將數組賦值到成員變量array中。
  3. 利用volatile關鍵字修飾成員變量array,這樣就可以保證array的引用的可見性,每次修改之前都能夠拿到最新的array引用。這點很關鍵。

看到這裡,相信你已經對CopyOnWriteArrayList非常了解了,CopyOnWriteArrayList在查詢多,修改操作少的情況下效率是非常可觀的,既能夠保證線程安全,又能有不錯的效率。但是如果修改操作較多,就會導致數組頻繁的copy,效率就會有所下降,如果修改操作很多,那麼直接使用Collections.synchronizedList(),或許也是一個不錯的選擇。

作者:沉迷學習的小伙
連結:https://juejin.cn/post/7143428003777740814
來源:稀土掘金

關鍵字: