只有光頭才能變強
前一陣子寫過一篇COW(Copy On Write)文章,結果閱讀量很低啊...COW奶牛!Copy On Write機制瞭解一下java
可能你們對這個技術比較陌生吧,但這項技術是挺多應用場景的。除了上文所說的Linux、文件系統外,其實在Java也有其身影。git
你們對線程安全容器可能最熟悉的就是ConcurrentHashMap了,由於這個容器常常會在面試的時候考查。github
好比說,一個常見的面試場景:面試
那若是有這樣的面試呢?編程
今天主要講解的是CopyOnWriteArrayList~數組
本文力求簡單講清每一個知識點,但願你們看完能有所收穫安全
咱們知道ArrayList是用於替代Vector的,Vector是線程安全的容器。由於它幾乎在每一個方法聲明處都加了synchronized關鍵字來使容器安全。服務器
若是使用Collections.synchronizedList(new ArrayList())
來使ArrayList變成是線程安全的話,也是幾乎都是每一個方法都加上synchronized關鍵字的,只不過它不是加在方法的聲明處,而是方法的內部。多線程
在講解CopyOnWrite容器以前,咱們仍是先來看一下線程安全容器的一些可能沒有注意到的地方~併發
下面咱們直接來看一下這段代碼:
// 獲得Vector最後一個元素 public static Object getLast(Vector list) { int lastIndex = list.size() - 1; return list.get(lastIndex); } // 刪除Vector最後一個元素 public static void deleteLast(Vector list) { int lastIndex = list.size() - 1; list.remove(lastIndex); }
以咱們第一反應來分析一下上面兩個方法:在多線程環境下,是否有問題?
size()和get()以及remove()
都被synchronized修飾的。答案:從調用者的角度是有問題的
咱們能夠寫段代碼測試一下:
import java.util.Vector; public class UnsafeVectorHelpers { public static void main(String[] args) { // 初始化Vector Vector<String> vector = new Vector(); vector.add("關注公衆號"); vector.add("Java3y"); vector.add("買Linux可到我下面的連接,享受最低價"); vector.add("給3y加雞腿"); new Thread(() -> getLast(vector)).start(); new Thread(() -> deleteLast(vector)).start(); new Thread(() -> getLast(vector)).start(); new Thread(() -> deleteLast(vector)).start(); } // 獲得Vector最後一個元素 public static Object getLast(Vector list) { int lastIndex = list.size() - 1; return list.get(lastIndex); } // 刪除Vector最後一個元素 public static void deleteLast(Vector list) { int lastIndex = list.size() - 1; list.remove(lastIndex); } }
能夠發現的是,有可能會拋出異常的:
緣由也很簡單,咱們照着流程走一下就行了:
getLast()
方法,線程B執行deleteLast()
方法int lastIndex = list.size() - 1;
獲得lastIndex的值是3。同時,線程B執行int lastIndex = list.size() - 1;
獲得的lastIndex的值也是3list.remove(lastIndex)
將下標爲3的元素刪除了list.get(lastIndex);
,發現已經沒有下標爲3的元素,拋出異常了.出現這個問題的緣由也很簡單:
getLast()
和deleteLast()
這兩個方法並非原子性的,即便他們內部的每一步操做是原子性的(被Synchronize修飾就能夠實現原子性),可是內部之間仍是能夠交替執行。
size()和get()以及remove()
都是原子性的,可是若是併發執行getLast()
和deleteLast()
,方法裏面的size()和get()以及remove()
是能夠交替執行的。要解決上面這種狀況也很簡單,由於咱們都是對Vector進行操做的,只要操做Vector前把它鎖住就沒毛病了!
因此咱們能夠改爲這樣子:
// 獲得Vector最後一個元素 public static Object getLast(Vector list) { synchronized (list) { int lastIndex = list.size() - 1; return list.get(lastIndex); } } // 刪除Vector最後一個元素 public static void deleteLast(Vector list) { synchronized (list) { int lastIndex = list.size() - 1; list.remove(lastIndex); } }
ps:若是有人去測試一下,發現會拋出異常java.lang.ArrayIndexOutOfBoundsException: -1,這是 沒有檢查角標的異常,不是併發致使的問題。
通過上面的例子咱們能夠看看下面的代碼:
public static void main(String[] args) { // 初始化Vector Vector<String> vector = new Vector(); vector.add("關注公衆號"); vector.add("Java3y"); vector.add("買Linux可到我下面的連接,享受最低價"); vector.add("給3y加雞腿"); // 遍歷Vector for (int i = 0; i < vector.size(); i++) { // 好比在這執行vector.clear(); //new Thread(() -> vector.clear()).start(); System.out.println(vector.get(i)); } }
一樣地:若是在遍歷Vector的時候,有別的線程修改了Vector的長度,那仍是會有問題!
vector.size()
時,發現Vector的長度爲5clear()
操做vector.get(i)
時,拋出異常在JDK5之後,Java推薦使用for-each
(迭代器)來遍歷咱們的集合,好處就是簡潔、數組索引的邊界值只計算一次。
若是使用for-each
(迭代器)來作上面的操做,會拋出ConcurrentModificationException異常
SynchronizedList在使用迭代器遍歷的時候一樣會有問題的,源碼已經提醒咱們要手動加鎖了。
若是想要完美解決上面所講的問題,咱們能夠在遍歷前加鎖:
// 遍歷Vector synchronized (vector) { for (int i = 0; i < vector.size(); i++) { vector.get(i); } }
有經驗的同窗就能夠知道:哇,遍歷一下容器都要我加上鎖,這這這不是要慢死了嗎.的確是挺慢的..
因此咱們的CopyOnWriteArrayList就登場了!
通常來講,咱們會認爲:CopyOnWriteArrayList是同步List的替代品,CopyOnWriteArraySet是同步Set的替代品。
不管是Hashtable-->ConcurrentHashMap,仍是說Vector-->CopyOnWriteArrayList。JUC下支持併發的容器與老一代的線程安全類相比,總結起來就是加鎖粒度的問題
因此通常來講,咱們都會使用JUC包下給咱們提供的線程安全容器,而不是使用老一代的線程安全容器。
下面咱們來看看CopyOnWriteArrayList是怎麼實現的,爲何使用迭代器遍歷的時候就不用額外加鎖,也不會拋出ConcurrentModificationException異常。
咱們仍是先來回顧一下COW:
若是有多個調用者(callers)同時請求相同資源(如內存或磁盤上的數據存儲),他們會共同獲取 相同的指針指向相同的資源,直到某個調用者 試圖修改資源的內容時,系統纔會 真正複製一份專用副本(private copy)給該調用者,而其餘調用者所見到的最初的資源仍然保持不變。 優勢是若是調用者 沒有修改該資源,就不會有副本(private copy)被創建,所以多個調用者只是讀取操做時能夠 共享同一份資源。
參考自維基百科:https://zh.wikipedia.org/wiki/%E5%AF%AB%E5%85%A5%E6%99%82%E8%A4%87%E8%A3%BD
以前寫博客的時候,若是是要看源碼,通常會翻譯一下源碼的註釋並用圖貼在文章上的。Emmm,發現閱讀體驗並非很好,因此我這裏就 直接歸納一下源碼註釋說了什麼吧。另外,若是使用IDEA的話,能夠下一個插件 Translation(免費好用).
歸納一下CopyOnWriteArrayList源碼註釋介紹了什麼:
/** 可重入鎖對象 */ final transient ReentrantLock lock = new ReentrantLock(); /** CopyOnWriteArrayList底層由數組實現,volatile修飾 */ private transient volatile Object[] array; /** * 獲得數組 */ final Object[] getArray() { return array; } /** * 設置數組 */ final void setArray(Object[] a) { array = a; } /** * 初始化CopyOnWriteArrayList至關於初始化數組 */ public CopyOnWriteArrayList() { setArray(new Object[0]); }
看起來挺簡單的,CopyOnWriteArrayList底層就是數組,加鎖就交由ReentrantLock來完成。
根據上面的分析咱們知道若是遍歷Vector/SynchronizedList
是須要本身手動加鎖的。
CopyOnWriteArrayList使用迭代器遍歷時不須要顯示加鎖,看看add()、clear()、remove()
與get()
方法的實現可能就有點眉目了。
首先咱們能夠看看add()
方法
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; // 將volatile Object[] array 的指向替換成新數組 setArray(newElements); return true; } finally { lock.unlock(); } }
經過代碼咱們能夠知道:在添加的時候就上鎖,並複製一個新數組,增長操做在新數組上完成,將array指向到新數組中,最後解鎖。
再來看看size()
方法:
public int size() { // 直接獲得array數組的長度 return getArray().length; }
再來看看get()
方法:
public E get(int index) { return get(getArray(), index); } final Object[] getArray() { return array; }
那再來看看set()
方法
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; // 將array引用指向新數組 setArray(newElements); } else { // Not quite a no-op; enssures volatile write semantics setArray(elements); } return oldValue; } finally { lock.unlock(); } }
對於remove()、clear()
跟set()和add()
是相似的,這裏我就再也不貼出代碼了。
總結:
經常使用的方法實現咱們已經基本瞭解了,但仍是不知道爲啥可以在容器遍歷的時候對其進行修改而不拋出異常。因此,來看一下他的迭代器吧:
// 1. 返回的迭代器是COWIterator public Iterator<E> iterator() { return new COWIterator<E>(getArray(), 0); } // 2. 迭代器的成員屬性 private final Object[] snapshot; private int cursor; // 3. 迭代器的構造方法 private COWIterator(Object[] elements, int initialCursor) { cursor = initialCursor; snapshot = elements; } // 4. 迭代器的方法... public E next() { if (! hasNext()) throw new NoSuchElementException(); return (E) snapshot[cursor++]; } //.... 能夠發現的是,迭代器全部的操做都基於snapshot數組,而snapshot是傳遞進來的array數組
到這裏,咱們應該就能夠想明白了!CopyOnWriteArrayList在使用迭代器遍歷的時候,操做的都是原數組!
看了上面的實現源碼,咱們應該也大概能分析出CopyOnWriteArrayList的缺點了。
內存佔用:若是CopyOnWriteArrayList常常要增刪改裏面的數據,常常要執行add()、set()、remove()
的話,那是比較耗費內存的。
add()、set()、remove()
這些增刪改操做都要複製一個數組出來。數據一致性:CopyOnWrite容器只能保證數據的最終一致性,不能保證數據的實時一致性。
setArray()
了)。可是線程A迭代出來的是原有的數據。CopyOnWriteArraySet的原理就是CopyOnWriteArrayList。
private final CopyOnWriteArrayList<E> al; public CopyOnWriteArraySet() { al = new CopyOnWriteArrayList<E>(); }
如今臨近雙十一買阿里雲服務器就特別省錢!以前我買學生機也要9.8塊錢一個月,如今最低價只須要8.3一個月!
若是有要買服務器的同窗可經過個人連接直接享受最低價:https://m.aliyun.com/act/team1111/#/share?params=N.FF7yxCciiM.pfn5xpli
閱讀這篇文章可能須要對Java容器和多線程有必定的瞭解。若是對這些知識還不太瞭解的同窗們可看我以前寫過的文章哦~
若是你們有更好的理解方式或者文章有錯誤的地方還請你們不吝在評論區留言,你們互相學習交流~~~
參考資料:
擴展閱讀:
一個 堅持原創的Java技術公衆號:Java3y,歡迎你們關注
3y全部的原創文章: