Java併發包提供了不少線程安全的集合,有了他們的存在,使得咱們在多線程開發下,能夠和單線程同樣去編寫代碼,大大簡化了多線程開發的難度,可是若是不知道其中的原理,可能會引起意想不到的問題,因此知道其中的原理仍是頗有必要的。數組
今天咱們來看下Java併發包中提供的線程安全的List,即CopyOnWriteArrayList。安全
剛接觸CopyOnWriteArrayList的時候,我總感受這個集合的名稱有點奇怪:在寫的時候複製?後來才知道它就是在寫的時候進行了複製,因此這個命名仍是至關嚴謹的。固然,翻譯成 寫時複製 會更好一些。微信
咱們在研究源碼的時候,能夠帶着問題去研究,這樣可能效果會更好,把問題一個一個攻破,也更有成就感,因此在這裏,我先拋出幾個問題:多線程
咱們先來看下CopyOnWriteArrayList的UML圖:
併發
咱們能夠經過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
解析源碼後,咱們明白了線程
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方法修改指定下標元素的值。
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方法後:
經過源碼解析,咱們應該更有體會:
咱們能夠經過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()); } } }
運行結果:
代碼很簡單,這裏就再也不解釋了,咱們直接來看迭代器的源碼:
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()); } }
運行結果:
這沒問題把,咱們先是往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()); } }
此次咱們改變了代碼的順序,先是獲取迭代器,而後是執行刪除線程的操做,最後遍歷迭代器。
運行結果:
能夠看到被刪除的元素,仍是打印出來了。
若是咱們沒有分析源碼,不知道其中的原理,不知道弱一致性,當在多線程中用到CopyOnWriteArrayList的時候,可能會痛不欲生,想砸電腦,不知道爲何獲取的數據有時候就不是正確的數據,而有時候又是。因此探究原理,仍是挺有必要的,不論是經過源碼分析,仍是經過看博客,甚至是直接看JDK中的註釋,都是能夠的。
在Java併發包提供的集合中,CopyOnWriteArrayList應該是最簡單的一個,但願經過源碼分析,讓你們有一個信心,原來JDK源碼也是能夠讀懂的。