CopyOnWriteArrayList源碼解析

Java併發包提供了不少線程安全的集合,有了他們的存在,使得咱們在多線程開發下,能夠和單線程同樣去編寫代碼,大大簡化了多線程開發的難度,可是若是不知道其中的原理,可能會引起意想不到的問題,因此知道其中的原理仍是頗有必要的。數組

今天咱們來看下Java併發包中提供的線程安全的List,即CopyOnWriteArrayList。安全

剛接觸CopyOnWriteArrayList的時候,我總感受這個集合的名稱有點奇怪:在寫的時候複製?後來才知道它就是在寫的時候進行了複製,因此這個命名仍是至關嚴謹的。固然,翻譯成 寫時複製 會更好一些。微信

咱們在研究源碼的時候,能夠帶着問題去研究,這樣可能效果會更好,把問題一個一個攻破,也更有成就感,因此在這裏,我先拋出幾個問題:多線程

  1. CopyOnWriteArrayList如何保證線程安全性的。
  2. CopyOnWriteArrayList長度有沒有限制。
  3. 爲何說CopyOnWriteArrayList是一個寫時複製集合。

咱們先來看下CopyOnWriteArrayList的UML圖:
併發

主要方法源碼解析

add

咱們能夠經過add方法添加一個元素源碼分析

public boolean add(E e) {
        //1.得到獨佔鎖
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            Object[] elements = getArray();//2.得到Object[]
            int len = elements.length;//3.得到elements的長度
            Object[] newElements = Arrays.copyOf(elements, len + 1);//4.複製到新的數組
            newElements[len] = e;//5.將add的元素添加到新元素
            setArray(newElements);//6.替換以前的數據
            return true;
        } finally {
            lock.unlock();//7.釋放獨佔鎖
        }
    }
final Object[] getArray() {
        return array;
    }

當調用add方法,代碼會跑到(1)去得到獨佔鎖,由於獨佔鎖的特性,致使若是有多個線程同時跑到(1),只能有一個線程成功得到獨佔鎖,而且執行下面的代碼,其他的線程只能在外面等着,直到獨佔鎖被釋放。ui

線程得到到獨佔鎖後,執行(2),得到array,而且賦值給elements ,(3)得到elements的長度,而且賦值給len,(4)複製elements數組,在此基礎上長度+1,賦值給newElements,(5)將咱們須要新增的元素添加到newElements,(6)替換以前的數組,最後跑到(7)釋放獨佔鎖。this

解析源碼後,咱們明白了線程

  1. CopyOnWriteArrayList是如何保證【寫】時線程安全的?由於用了ReentrantLock獨佔鎖,保證同時只有一個線程對集合進行修改操做。
  2. 數據是存儲在CopyOnWriteArrayList中的array數組中的。
  3. 在添加元素的時候,並非直接往array裏面add元素,而是複製出來了一個新的數組,而且複製出來的數組的長度是 【舊數組的長度+1】,再把舊的數組替換成新的數組,這是尤爲須要注意的。

get

public E get(int index) {
        return get(getArray(), index);
    }
final Object[] getArray() {
        return array;
    }

咱們能夠經過調用get方法,來得到指定下標的元素。翻譯

首先得到array,而後得到指定下標的元素,看起來沒有任何問題,可是其實這是存在問題的。別忘了,咱們如今是多線程的開發環境,否則也沒有必要去使用JUC下面的東西了。

試想這樣的場景,當咱們得到了array後,把array捧在手內心,如獲珍寶。。。因爲整個get方法沒有獨佔鎖,因此另一個線程還能夠繼續執行修改的操做,好比執行了remove的操做,remove和add同樣,也會申請獨佔鎖,而且複製出新的數組,刪除元素後,替換掉舊的數組。而這一切get方法是不知道的,它不知道array數組已經發生了天翻地覆的變化,它仍是傻乎乎的,看着捧在手內心的array。。。這就是弱一致性

就像微信同樣,雖然對方已經把你給刪了,可是你不知道,你仍是天天打開和她的聊天框,準備說些什麼。。。

set

咱們能夠經過set方法修改指定下標元素的值。

public E set(int index, E element) {
        //(1)得到獨佔鎖
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            Object[] elements = getArray();//(2)得到array
            E oldValue = get(elements, index);//(3)根據下標,得到舊的元素

            if (oldValue != element) {//(4)若是舊的元素不等於新的元素
                int len = elements.length;//(5)得到舊數組的長度
                Object[] newElements = Arrays.copyOf(elements, len);//(6)複製出新的數組
                newElements[index] = element;//(7)修改
                setArray(newElements);//(8)替換
            } else {
                //(9)爲了保證volatile 語義,即便沒有修改,也要替換成新的數組
                // Not quite a no-op; ensures volatile write semantics
                setArray(elements);
            }
            return oldValue;
        } finally {
            lock.unlock();//(10)釋放獨佔鎖
        }
    }

當咱們調用set方法後:

  1. 和add方法同樣,先獲取獨佔鎖,一樣的,只有一個線程能夠得到獨佔鎖,其餘線程會被阻塞。
  2. 獲取到獨佔鎖的線程得到array,而且賦值給elements。
  3. 根據下標,得到舊的元素。
  4. 進行一個對比,檢查舊的元素是否不等於新的元素,若是成立的話,執行5-8,若是不成立的話,執行9。
  5. 得到舊數組的長度。
  6. 複製出新的數組。
  7. 修改新的數組中指定下標的元素。
  8. 把舊的數組替換掉。
  9. 爲了保證volatile語義,即便沒有修改,也要替換成新的數組。
  10. 不論是否執行了修改的操做,都會釋放獨佔鎖。

經過源碼解析,咱們應該更有體會:

  1. 經過獨佔鎖,來保證【寫】的線程安全。
  2. 修改操做,實際上操做的是array的一個副本,最後才把array給替換掉。

remove

咱們能夠經過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();
        }
    }

能夠看到,remove方法和add,set方法是同樣的,第一步仍是先獲取獨佔鎖,來保證線程安全性,若是要刪除的元素是最後一個,則複製出一個長度爲【舊數組的長度-1】的新數組,隨之替換,這樣就巧妙的把最後一個元素給刪除了,若是要刪除的元素不是最後一個,則分兩次複製,隨之替換。

迭代器

在解析源碼前,咱們先看下迭代器的基本使用:

public class Main {public static void main(String[] args) {
        CopyOnWriteArrayList<String> copyOnWriteArrayList=new CopyOnWriteArrayList<>();
        copyOnWriteArrayList.add("Hello");
        copyOnWriteArrayList.add("copyOnWriteArrayList");
        Iterator<String>iterator=copyOnWriteArrayList.iterator();
        while (iterator.hasNext()){
            System.out.println(iterator.next());
        }
    }
}

運行結果:
image.png

代碼很簡單,這裏就再也不解釋了,咱們直接來看迭代器的源碼:

public Iterator<E> iterator() {
        return new COWIterator<E>(getArray(), 0);
    }
static final class COWIterator<E> implements ListIterator<E> {
    
        private final Object[] snapshot;
     
        private int cursor;

        private COWIterator(Object[] elements, int initialCursor) {
            cursor = initialCursor;
            snapshot = elements;
        }
        
        // 判斷是否還有下一個元素
        public boolean hasNext() {
            return cursor < snapshot.length;
        }
        
        //獲取下個元素
        @SuppressWarnings("unchecked")
        public E next() {
            if (! hasNext())
                throw new NoSuchElementException();
            return (E) snapshot[cursor++];
        }

當咱們調用iterator方法獲取迭代器,內部會調用COWIterator的構造方法,此構造方法有兩個參數,第一個參數就是array數組,第二個參數是下標,就是0。隨後構造方法中會把array數組賦值給snapshot變量。
snapshot是「快照」的意思,若是Java基礎尚可的話,應該知道數組是引用類型,傳遞的是指針,若是有其餘地方修改了數組,這裏應該立刻就能夠反應出來,那爲何又會是snapshot這樣的命名呢?沒錯,若是其餘線程沒有對CopyOnWriteArrayList進行增刪改的操做,那麼snapshot就是自己的array,可是若是其餘線程對CopyOnWriteArrayList進行了增刪改的操做,舊的數組會被新的數組給替換掉,可是snapshot仍是原來舊的數組的引用。也就是說 當咱們使用迭代器便利CopyOnWriteArrayList的時候,不能保證拿到的數據是最新的,這也是弱一致性問題。

什麼?你不信?那咱們經過一個demo來證明下:

public static void main(String[] args) throws InterruptedException {
        CopyOnWriteArrayList<String> copyOnWriteArrayList=new CopyOnWriteArrayList<>();
        copyOnWriteArrayList.add("Hello");
        copyOnWriteArrayList.add("CopyOnWriteArrayList");
        copyOnWriteArrayList.add("2019");
        copyOnWriteArrayList.add("good good study");
        copyOnWriteArrayList.add("day day up");
        new Thread(()->{
            copyOnWriteArrayList.remove(1);
            copyOnWriteArrayList.remove(3);
        }).start();
        TimeUnit.SECONDS.sleep(3);
        Iterator<String> iterator = copyOnWriteArrayList.iterator();
        while (iterator.hasNext()) {
            System.out.println(iterator.next());
        }
    }

運行結果:
image.png
這沒問題把,咱們先是往list裏面add了點數據,而後開一個線程,在線程裏面刪除一些元素,睡3秒是爲了保證線程運行完畢。而後獲取迭代器,遍歷元素,發現被remove的元素沒有被打印出來。

而後咱們換一種寫法:

public static void main(String[] args) throws InterruptedException {
        CopyOnWriteArrayList<String> copyOnWriteArrayList=new CopyOnWriteArrayList<>();
        copyOnWriteArrayList.add("Hello");
        copyOnWriteArrayList.add("CopyOnWriteArrayList");
        copyOnWriteArrayList.add("2019");
        copyOnWriteArrayList.add("good good study");
        copyOnWriteArrayList.add("day day up");
        Iterator<String> iterator = copyOnWriteArrayList.iterator();
        new Thread(()->{
            copyOnWriteArrayList.remove(1);
            copyOnWriteArrayList.remove(3);
        }).start();
        while (iterator.hasNext()) {
            System.out.println(iterator.next());
        }
    }

此次咱們改變了代碼的順序,先是獲取迭代器,而後是執行刪除線程的操做,最後遍歷迭代器。
運行結果:
image.png
能夠看到被刪除的元素,仍是打印出來了。

若是咱們沒有分析源碼,不知道其中的原理,不知道弱一致性,當在多線程中用到CopyOnWriteArrayList的時候,可能會痛不欲生,想砸電腦,不知道爲何獲取的數據有時候就不是正確的數據,而有時候又是。因此探究原理,仍是挺有必要的,不論是經過源碼分析,仍是經過看博客,甚至是直接看JDK中的註釋,都是能夠的。

在Java併發包提供的集合中,CopyOnWriteArrayList應該是最簡單的一個,但願經過源碼分析,讓你們有一個信心,原來JDK源碼也是能夠讀懂的。

相關文章
相關標籤/搜索