1. 程式人生 > >【轉】Java併發程式設計筆記之CopyOnWriteArrayList原始碼分析

【轉】Java併發程式設計筆記之CopyOnWriteArrayList原始碼分析

併發包中併發List只有CopyOnWriteArrayList這一個,CopyOnWriteArrayList是一個執行緒安全的ArrayList,對其進行修改操作和元素迭代操作都是在底層建立一個拷貝陣列(快照)上進行的,也就是寫時拷貝策略。

我們首先看一下CopyOnWriteArrayList的類圖有哪些屬性和方法,如下圖所示:

如上,CopyOnWriteArrayList的類圖,每個CopyOnWriteArrayList物件裡面有一個array陣列物件用來存放具體元素,ReentrantLock獨佔鎖物件用來保證同時只有一個執行緒對array進行修改,這裡只要記得ReentrantLock是獨佔鎖,同時只有一個執行緒可以獲取就可以了。

那麼問題來了,如果讓我們自己去做一個寫時拷貝的執行緒安全的List,我們會怎麼做,要考慮哪些要點?

  1.list何時初始化,初始化list元素個數為多少,list是有限大小?

  2.如何保證執行緒安全,比如多個執行緒進行讀寫時候,如果保證是執行緒安全的?

  3.如何使用迭代器遍歷list時候的資料一致性?

那麼我們就進入CopyOnWriteArrayList原始碼去看,看看設計者是如何處理的。

CopyOnWriteArrayList原始碼分離如下:

1.初始化,首先看一下CopyOnWriteArrayList的無參建構函式,如下程式碼內部建立了一個大小為0的Object資料作為array的初始值。原始碼如下:

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

接下來再看看CopyOnWriteArrayList的有參建構函式,原始碼如下:

複製程式碼

    //建立一個list,其內部元素是入參toCopyIn的拷貝
    public CopyOnWriteArrayList(E[] toCopyIn) {
        setArray(Arrays.copyOf(toCopyIn, toCopyIn.length, Object[].class));
    }

    //入參為集合,拷貝集合裡面元素到本list
    public CopyOnWriteArrayList(Collection<? extends E> c) {
        Object[] elements;
        if (c.getClass() == CopyOnWriteArrayList.class)
            elements = ((CopyOnWriteArrayList<?>)c).getArray();
        else {
            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);
    }

複製程式碼

2.新增元素,CopyOnWriteArrayList中新增元素函式有add(E e) , add(int index , E element) ,addIfAbsent(E e) , addAllAbsent(Collection<? extends E> c) 等操作,原理一致,

接下來就以add(E e)進行講解。原始碼如下:

複製程式碼

public boolean add(E e) {

        //加獨佔鎖(1)
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            //獲取array(2)
            Object[] elements = getArray();

            //拷貝array到新陣列,新增元素到新陣列(3)
            int len = elements.length;
            Object[] newElements = Arrays.copyOf(elements, len + 1);
            newElements[len] = e;

            //使用新陣列替換新增前的陣列(4)
            setArray(newElements);
            return true;
        } finally {
            //釋放獨佔鎖(5)
            lock.unlock();
        }
 }

複製程式碼

如上程式碼,呼叫add方法的執行緒會首先執行程式碼(1)去獲取獨佔鎖,如果多個執行緒都呼叫add則只有一個執行緒會獲取該鎖,其他執行緒會被阻塞掛起知道鎖被釋放。所以一個執行緒獲取到鎖後,就保證了在該執行緒新增元素過程中,其他執行緒不會對array進行修改。

執行緒獲取鎖後執行程式碼(2)獲取array,然後執行程式碼(3)拷貝array到一個新陣列(從這裡可以知道新陣列的大小是原來陣列大小加增加1,所以CopyOnWriteArrayList是無界List),並把要新增的元素新增到新陣列。

然後執行到程式碼(4)把新陣列替換原陣列,然後返回前要釋放鎖,由於加了鎖,所以整個add過程是個原子操作,需要注意的是新增元素時候首先是拷貝了一個快照,然後在快照上進行的新增,而不是直接在源來的陣列上進行。

3.獲取指定位置元素,E get(int index)獲取下標為index的元素,如果元素不存在會丟擲IndexOutOfBoundsException 異常,原始碼如下:

複製程式碼

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];
    }
}

複製程式碼

如上程式碼,獲取指定位置的元素分為兩步,首先獲取到當前list裡面的array陣列,這裡稱為步驟1,然後通過隨機訪問的下標方式訪問指定位置的元素,這裡稱為步驟2。

從程式碼中可以看到整個過程並沒有加鎖,這就可能會導致當執行完步驟1後執行步驟2前,另外一個執行緒C進行了修改操作,比如remove操作,就會進行寫時拷貝刪除當前get方法要訪問的元素,並且修改當前list的array為新陣列。

而這之後步驟2 可能才開始執行,步驟2操作的是執行緒C刪除元素前的一個快照陣列(因為步驟1讓array指向的是原來的陣列),所以雖然執行緒C已經刪除了index處的元素,但是步驟2還是返回index處的元素,這其實就是寫時拷貝策略帶來弱一致性。

4.修改指定元素,修改 list 中指定元素的值,如果指定位置的元素不存在則丟擲 IndexOutOfBoundsException 異常,原始碼碼如下:

複製程式碼

  public E set(int index, E element) {
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            Object[] elements = getArray();
            E oldValue = get(elements, index);

            if (oldValue != element) {
                int len = elements.length;
                Object[] newElements = Arrays.copyOf(elements, len);
                newElements[index] = element;
                setArray(newElements);
            } else {
                // Not quite a no-op; ensures volatile write semantics
                setArray(elements);
            }
            return oldValue;
        } finally {
            lock.unlock();
        }
    }

複製程式碼

如上程式碼,首先獲取了獨佔鎖控制了其他執行緒對array陣列的修改,然後獲取當前陣列,並呼叫get方法獲取指定位置元素。

如果指定的位置元素與新值不一致則建立新陣列並拷貝元素,在新陣列上修改指定位置元素值並設定新陣列到array。

如果指定位置元素與新值一樣,則為了保障volatile語義,還是需要重新設定下array,雖然array內容並沒有改變(為了保證 volatile 語義是考慮到 set 方法本身應該提供 volatile 的語義).

5.刪除元素,刪除list裡面指定的元素,主要的方法有如下方法:

  E remove(int index)

  boolean remove(Object o)

  boolean remove(Object o, Object[] snapshot, int index) 等方法,原理一致,這裡講解下 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)
                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();
        }
    }

複製程式碼

正如上面程式碼所示,其實和新增元素時候是類似的,首先是獲取獨佔鎖保證刪除陣列期間,其他執行緒不能對array進行修改,然後獲取陣列中要給刪除的元素,並把剩餘的原始拷貝到新陣列後,把新陣列替換原來的陣列,最後在返回前釋放鎖。

6.接下來要講解一下弱一致性的迭代器。

遍歷列表元素可以使用迭代器進行迭代操作,講解什麼是迭代器的弱一致性前先上一個例子說明下迭代器的使用。程式碼如下:

複製程式碼

import java.util.concurrent.CopyOnWriteArrayList;

/**
 * Created by cong on 2018/6/9.
 */
public class CopyOnWriteArrayListTest {
    public static void main( String[] args ) {
        CopyOnWriteArrayList<String> arrayList = new CopyOnWriteArrayList<>();
        arrayList.add("hello");
        arrayList.add("java");

        Iterator<String> itr = arrayList.iterator();
        while(itr.hasNext()){
            System.out.println(itr.next());
        }

    }
    
}

複製程式碼

執行結果如下:

其中迭代器的hasNext方法用來判斷是否還有元素,next方法則是具體返回元素。那麼接下來看CopyOnWriteArrayList中迭代器是弱一致性,所謂弱一致性是指返回迭代器後,其他執行緒對list的增刪改對迭代器不可見,無感知的,那麼下面就看看是如何做到的。原始碼如下:

複製程式碼

public Iterator<E> iterator() {
    return new COWIterator<E>(getArray(), 0);
}

static final class COWIterator<E> implements ListIterator<E> {
    //array的快照版本
    private final Object[] snapshot;

    //陣列下標
    private int cursor;

    //建構函式
    private COWIterator(Object[] elements, int initialCursor) {
        cursor = initialCursor;
        snapshot = elements;
    }

    //是否遍歷結束
    public boolean hasNext() {
        return cursor < snapshot.length;
    }

    //獲取元素
    public E next() {
        if (! hasNext())
            throw new NoSuchElementException();
        return (E) snapshot[cursor++];
    }
}

複製程式碼

如上程式碼當呼叫iterator()方法獲取迭代器時候實際是返回一個COWIterator物件,COWIterator的snapshot變數儲存了當前list的內容,cursor是遍歷list資料的下標。

這裡為什麼說snapshot是list的快照呢?明明是指標傳遞的引用,而不是拷貝。如果在該執行緒使用返回的迭代器遍歷元素的過程中,其他執行緒沒有對list進行增刪改,那麼snapshot本身就是list的array,因為它們是引用關係。

但是如果遍歷期間,有其他執行緒對該list進行了增刪改,那麼snapshot就是快照了,因為增刪改後list裡面的陣列被新陣列替換了,這時候老陣列只有被snapshot索引用,所以這也就說明獲取迭代器後,使用改迭代器進行變數元素時候,其它執行緒對該list進行的增刪改是不可見的,

因為它們操作的是兩個不同的陣列,這也就是弱一致性的達成。

下面通過一個例子來演示多執行緒下,迭代器的弱一致性的效果:程式碼如下:

複製程式碼

import java.util.Iterator;
import java.util.concurrent.CopyOnWriteArrayList;

/**
 * Created by cong on 2018/6/9.
 */
public class CopyOnWriteArrayListTest {
    private static volatile CopyOnWriteArrayList<String> arrayList = new CopyOnWriteArrayList<>();

    public static void main( String[] args ) throws InterruptedException
    {
        arrayList.add("hello");
        arrayList.add("Java");
        arrayList.add("welcome");
        arrayList.add("to");
        arrayList.add("hangzhou");

        Thread threadOne = new Thread(new Runnable() {

            @Override
            public void run() {

                //修改list中下標為1的元素為JavaSe
                arrayList.set(1, "JavaSe");
                //刪除元素
                arrayList.remove(2);
                arrayList.remove(3);

            }
        });

        //保證在修改執行緒啟動前獲取迭代器
        Iterator<String> itr = arrayList.iterator();

        //啟動執行緒
        threadOne.start();

        //等在子執行緒執行完畢
        threadOne.join();

        //迭代元素
        while(itr.hasNext()){
            System.out.println(itr.next());
        }

    }

}

複製程式碼

執行結果如下:

如上程式碼main函式首先初始化了arrayList,然後在啟動執行緒前獲取到了arrayList迭代器,子執行緒ThreadOne啟動後首先修改了arrayList第一個元素的值,然後刪除了arrayList中座標為2,3 的元素。

主執行緒等待子執行緒執行完畢後使用獲取的迭代器遍歷陣列元素,從列印的結果來看,子執行緒裡面進行的操縱是一個都沒有生效,這就是迭代器的弱一致性的效果,需要注意的是獲取迭代器必須在子執行緒操作之前進行。

注意:CopyOnWriteArrayList使用寫時拷貝的策略來保證list的一致性,而獲取-拷貝-寫入 三步並不是原子性的,所以在修改增刪改的過程中都是用了獨佔鎖,並保證了同時只有一個執行緒才能對list陣列進行修改。

另外CopyOnWriteArrayList提供了弱一致性的迭代器,保證在獲取迭代器後,其他執行緒對list的修改該不可見,迭代器遍歷時候的陣列是獲取迭代器時候的一個快照,另外併發包中CopyOnWriteArraySet 底層就是使用它進行實現,感興趣的可以去翻翻看。