**面試官:** 小夥子你有點眼熟啊,是否是去年來這面試過啊。<br/>java
**二胖:** 啊,沒有啊我這是第一次來這。<br/>面試
**面試官:** 行,那咱們開始今天的面試吧,剛開始咱們先來點簡單的吧,java
裏面的容器你知道哪些啊,跟我說一說吧。<br/>數組
**二胖:** 好的,java裏面常見容器有ArrayList
(線程非安全)、HashMap
(線程非安全)、HashSet
(線程非安全),ConcurrentHashMap
(線程安全)。<br/>緩存
**面試官:** ArrayList
既然線程非安全那有沒有線程安全的ArrayList
列?<br/>安全
**二胖:** 這個。。。 好像問到知識盲點了。<br/>多線程
**面試官:** 那咱們今天的面試就先到這了,我待會還有一個會,後續若有通知人事會聯繫你的。<br/>併發
**以上故事純屬虛構若有雷同請以本文爲主。**app
在java裏面說到集合容器咱們通常首先會想到的是HashMap
、ArrayList
、HasHSet
這幾個容器也是平時開發中用的最多的。dom
這幾個都是非線程安全的,若是咱們有特定業務須要使用線程的安全容器列,ide
HashMap
能夠用ConcurrentHashMap
代替。
ArrayList
可使用Collections.synchronizedList()
方法(list
每一個方法都用synchronized
修飾) 或者使用Vector
(如今基本也不用了,每一個方法都用synchronized
修飾)或者使用CopyOnWriteArrayList
替代。
Collections.synchronizedSet
或者使用CopyOnWriteArraySet
來代替。(CopyOnWriteArraySet爲何不叫CopyOnWriteHashSet由於CopyOnWriteArraySet
底層是採用CopyOnWriteArrayList
來實現的)咱們能夠看到CopyOnWriteArrayList
在線程安全的容器裏面屢次出現。
首先咱們來看看什麼是CopyOnWrite
?Copy-On-Write
簡稱COW
,是一種用於程序設計中的優化策略。
CopyOnWrite容器即寫時複製的容器。通俗的理解是當咱們往一個容器添加元素的時候,不直接往當前容器添加,而是先將當前容器進行Copy,複製出一個新的容器,而後新的容器裏添加元素,添加完元素以後,再將原容器的引用指向新的容器。這樣作的好處是咱們能夠對CopyOnWrite容器進行併發的讀,而不須要加鎖,由於當前容器不會添加任何元素。因此CopyOnWrite容器也是一種讀寫分離的思想,讀和寫不一樣的容器。
在java裏面咱們若是採用不正確的循環姿式去遍歷List時候,若是一邊遍歷一邊修改拋出java.util.ConcurrentModificationException
錯誤的。
若是對ArrayList循環遍歷不是很熟悉的能夠建議看下這篇文章《ArrayList的刪除姿式你都掌握了嗎》
List<String> list = new ArrayList<>(); list.add("張三"); list.add("java金融"); list.add("javajr.cn"); Iterator<String> iterator = list.iterator(); while(iterator.hasNext()){ String content = iterator.next(); if("張三".equals(content)) { list.remove(content); } }
上面這個栗子是會發生java.util.ConcurrentModificationException
異常的,若是把ArrayList
改成CopyOnWriteArrayList
是不會發生生異常的。
咱們再看下面一個栗子一個線程往List裏面添加數據,一個線程循環list讀數據。
List<String> list = new ArrayList<>(); list.add("張三"); list.add("java金融"); list.add("javajr.cn"); Thread t = new Thread(new Runnable() { int count = 0; @Override public void run() { while (true) { list.add(count++ + ""); } } }); t.start(); Thread.sleep(10000); for (String s : list) { System.out.println(s); }
咱們運行上述代碼也會發生ConcurrentModificationException
異常,若是把ArrayList
換成了CopyOnWriteArrayList
就一切正常。
經過上面兩個栗子咱們能夠發現CopyOnWriteArrayList
是線程安全的,下面咱們就來一塊兒看看CopyOnWriteArrayList
是如何實現線程安全的。
public class CopyOnWriteArrayList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable { private static final long serialVersionUID = 8673264195747942595L; /\*\* The lock protecting all mutators \*/ final transient ReentrantLock lock = new ReentrantLock(); /\*\* The array, accessed only via getArray/setArray. \*/ private transient volatile Object[] array;
從源碼中咱們能夠知道CopyOnWriteArrayList
這和ArrayList
底層實現都是經過一個Object
的數組來實現的,只不過 CopyOnWriteArrayList
的數組是經過volatile
來修飾的,爲何須要volatile
修飾建議能夠看看《Java的synchronized 能防止指令重排序嗎?》
還有新增了ReentrantLock
。
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(); } }
上述源碼咱們能夠發現比較簡單,有幾個點須要稍微注意下
增長數據的時候是經過ReentrantLock
加鎖操做來(在jdk11
的時候採用了synchronized
來替換ReentrantLock
)保證多線程寫的時候只有一個線程進行數組的複製,不然的話內存中會有多份被複制的數據,致使數據錯亂。
數組是經過volatile
修飾的,根據 volatile
的 happens-before
規則,寫線程對數組引用的修改是能夠當即對讀線程是可見的。
再Java併發包裏提供了兩個使用CopyOnWrite
機制實現的併發容器,它們是CopyOnWriteArrayList
和CopyOnWriteArraySet
,可是並無CopyOnWriteHashMap
咱們能夠按照他的思路本身來實現一個CopyOnWriteHashMap
public class CopyOnWriteHashMap<K, V> implements Map<K, V>, Cloneable { final transient ReentrantLock lock = new ReentrantLock(); private volatile Map<K, V> map; public CopyOnWriteHashMap() { map = new HashMap<>(); } @Override public V put(K key, V value) { final ReentrantLock lock = this.lock; lock.lock(); try { Map<K, V> newMap = new HashMap<K, V>(map); V val = newMap.put(key, value); map = newMap; return val; } finally { lock.unlock(); } } @Override public V get(Object key) { return map.get(key); } @Override public V remove(Object key) { final ReentrantLock lock = this.lock; lock.lock(); try { Map<K, V> newMap = new HashMap<K, V>(map); if (!newMap.containsKey(key)) { return null; } V v = newMap.get(key); newMap.remove(key); map = newMap; return v; }finally { lock.unlock(); } }
上述咱們實現了一個簡單的CopyOnWriteHashMap
,只實現了add、remove、get
方法其餘剩餘的方法能夠自行去實現,涉及到只要數據變化的就要加鎖,讀無需加鎖。
CopyOnWrite
併發容器適用於讀多寫少的併發場景,好比黑白名單、國家城市等基礎數據緩存、系統配置等。這些基本都是隻要想項目啓動的時候初始化一次,變動頻率很是的低。若是這種讀多寫少的場景採用 Vector,Collections
包裝的這些方式是不合理的,由於儘管多個讀線程從同一個數據容器中讀取數據,可是讀線程對數據容器的數據並不會發生發生修改,因此並不須要讀也加鎖。
CopyOnWriteArrayList雖然是一個線程安全版的ArrayList,但其每次修改數據時都會複製一份數據出來,因此CopyOnWriteArrayList只適用讀多寫少或無鎖讀場景。咱們若是在實際業務中使用CopyOnWriteArrayList,必定是由於這個場景適合而非是爲了炫技。
由於CopyOnWrite的寫時複製機制每次進行寫操做的時候都會有兩個數組對象的內存,若是這個數組對象佔用的內存較大的話,若是頻繁的進行寫入就會形成頻繁的Yong GC和Full GC。
CopyOnWrite容器只能保證數據的最終一致性,不能保證數據的實時一致性。讀操做的線程可能不會當即讀取到新修改的數據,由於修改操做發生在副本上。但最終修改操做會完成並更新容器因此這是最終一致性。
簡單的測試了下CopyOnWriteArrayList 和 Collections.synchronizedList()的讀和寫發現:
在高併發的寫時CopyOnWriteArray比同步Collections.synchronizedList慢百倍
在高併發的讀性能時CopyOnWriteArray比同步Collections.synchronizedList快幾十倍。
高併發寫時,CopyOnWriteArrayList爲什麼這麼慢呢?由於其每次add時,都用Arrays.copyOf建立新數組,頻繁add時內存申請釋放性能消耗大。
選擇CopyOnWriteArrayList的時候必定是讀遠大於寫。若是讀寫都差很少的話建議選擇Collections.synchronizedList。
因爲本身才疏學淺,不免會有紕漏,假如你發現了錯誤的地方,還望留言給我指出來,我會對其加以修正。
若是你以爲文章還不錯,你的轉發、分享、讚揚、點贊、留言就是對我最大的鼓勵。
巨人肩膀摘蘋果
http://ifeve.com/java-copy-on-write/