Python實戰社羣
Java實戰社羣
長按識別下方二維碼,按需求添加
掃碼關注添加客服
進Python社羣▲
掃碼關注添加客服
進Java社羣▲
作者丨碼農二胖
來源丨java金融
引言
面試官: 小夥子你有點眼熟啊,是不是去年來這面試過啊。
二胖: 啊,沒有啊我這是第一次來這。
面試官: 行,那我們開始今天的面試吧,剛開始我們先來點簡單的吧,java
裏面的容器你知道哪些啊,跟我說一說吧。
二胖: 好的,java裏面常見容器有ArrayList
(線程非安全)、HashMap
(線程非安全)、HashSet
(線程非安全),ConcurrentHashMap
(線程安全)。
面試官: ArrayList
既然線程非安全那有沒有線程安全的ArrayList
列?
二胖: 這個。。。好像問到知識盲點了。
面試官: 那我們今天的面試就先到這了,我待會還有一個會,後續如有通知人事會聯繫你的。
以上故事純屬虛構如有雷同請以本文爲主。
什麼是COW
在java裏面說到集合容器我們一般首先會想到的是HashMap
、ArrayList
、HasHSet
這幾個容器也是平時開發中用的最多的。這幾個都是非線程安全的,如果我們有特定業務需要使用線程的安全容器列,
HashMap
可以用ConcurrentHashMap
代替。ArrayList
可以使用Collections.synchronizedList()
方法(list
每個方法都用synchronized
修飾) 或者使用Vector
(現在基本也不用了,每個方法都用synchronized
修飾) 或者使用CopyOnWriteArrayList
替代。HasHSet 可以使用
Collections.synchronizedSet
或者使用CopyOnWriteArraySet
來代替。(CopyOnWriteArraySet爲什麼不叫CopyOnWriteHashSet因爲CopyOnWriteArraySet
底層是採用CopyOnWriteArrayList
來實現的) 我們可以看到CopyOnWriteArrayList
在線程安全的容器裏面多次出現。首先我們來看看什麼是CopyOnWrite
?Copy-On-Write
簡稱COW
,是一種用於程序設計中的優化策略。
CopyOnWrite容器即寫時複製的容器。通俗的理解是當我們往一個容器添加元素的時候,不直接往當前容器添加,而是先將當前容器進行Copy,複製出一個新的容器,然後新的容器裏添加元素,添加完元素之後,再將原容器的引用指向新的容器。這樣做的好處是我們可以對CopyOnWrite容器進行併發的讀,而不需要加鎖,因爲當前容器不會添加任何元素。所以CopyOnWrite容器也是一種讀寫分離的思想,讀和寫不同的容器。
爲什麼要引入COW
防止ConcurrentModificationException異常
在java裏面我們如果採用不正確的循環姿勢去遍歷List時候,如果一邊遍歷一邊修改拋出java.util.ConcurrentModificationException
錯誤的。
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
是線程安全的,下面我們就來一起看看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
來修飾的,還有新增了ReentrantLock
。
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;
// 把新數組的值 賦給原數組
setArray(newElements);
return true;
} finally {
// 釋放鎖
lock.unlock();
}
}
上述源碼我們可以發現比較簡單,有幾個點需要稍微注意下
增加數據的時候是通過
ReentrantLock
加鎖操作來(在jdk11
的時候採用了synchronized
來替換ReentrantLock
)保證多線程寫的時候只有一個線程進行數組的複製,否則的話內存中會有多份被複制的數據,導致數據錯亂。數組是通過
volatile
修飾的,根據volatile
的happens-before
規則,寫線程對數組引用的修改是可以立即對讀線程是可見的。通過寫時複製來保證讀寫在兩個不同的數據容器中進行操作。
自己實現一個COW容器
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
包裝的這些方式是不合理的,因爲儘管多個讀線程從同一個數據容器中讀取數據,但是讀線程對數據容器的數據並不會發生發生修改,所以並不需要讀也加鎖。
CopyOnWrite缺點
CopyOnWriteArrayList雖然是一個線程安全版的ArrayList,但其每次修改數據時都會複製一份數據出來,所以CopyOnWriteArrayList只適用讀多寫少或無鎖讀場景。我們如果在實際業務中使用CopyOnWriteArrayList,一定是因爲這個場景適合而非是爲了炫技。
內存佔用問題
因爲CopyOnWrite的寫時複製機制每次進行寫操作的時候都會有兩個數組對象的內存,如果這個數組對象佔用的內存較大的話,如果頻繁的進行寫入就會造成頻繁的Yong GC和Full GC。
數據一致性問題
CopyOnWrite容器只能保證數據的最終一致性,不能保證數據的實時一致性。讀操作的線程可能不會立即讀取到新修改的數據,因爲修改操作發生在副本上。但最終修改操作會完成並更新容器所以這是最終一致性。
CopyOnWriteArrayList和Collections.synchronizedList()
簡單的測試了下CopyOnWriteArrayList 和 Collections.synchronizedList()的讀和寫發現:
在高併發的寫時CopyOnWriteArray比同步Collections.synchronizedList慢百倍
在高併發的讀性能時CopyOnWriteArray比同步Collections.synchronizedList快幾十倍。
高併發寫時,CopyOnWriteArrayList爲何這麼慢呢?因爲其每次add時,都用Arrays.copyOf創建新數組,頻繁add時內存申請釋放性能消耗大。
高併發讀的時候CopyOnWriteArray無鎖,Collections.synchronizedList有鎖所以讀的效率比較低下。
總結
選擇CopyOnWriteArrayList的時候一定是讀遠大於寫。如果讀寫都差不多的話建議選擇Collections.synchronizedList。
結束
由於自己才疏學淺,難免會有紕漏,假如你發現了錯誤的地方,還望留言給我指出來,我會對其加以修正。
如果你覺得文章還不錯,你的轉發、分享、讚賞、點贊、留言就是對我最大的鼓勵。
感謝您的閱讀,十分歡迎並感謝您的關注。
巨人肩膀摘蘋果
http://ifeve.com/java-copy-on-write/
程序員專欄 掃碼關注填加客服 長按識別下方二維碼進羣
近期精彩內容推薦:
在看點這裏好文分享給更多人↓↓