特別感謝:慕課網jimin老師的《Java併發編程與高併發解決方案》課程,以下知識點多數來自老師的課程內容。
jimin老師課程地址:Java併發編程與高併發解決方案
概述
Java併發容器JUC是三個單詞的縮寫。是JDK下面的一個包名。即Java.util.concurrency。
上一節我們介紹了ArrayList、HashMap、HashSet對應的同步容器保證其線程安全,這節我們介紹一下其對應的併發容器。
ArrayList –> CopyOnWriteArrayList
CopyOnWriteArrayList 寫操作時複製,當有新元素添加到集合中時,從原有的數組中拷貝一份出來,然後在新的數組上作寫操作,將原來的數組指向新的數組。整個數組的add操作都是在鎖的保護下進行的,防止併發時複製多份副本。讀操作是在原數組中進行,不需要加鎖
缺點:
1.寫操作時複製消耗內存,如果元素比較多時候,容易導致young gc 和full gc。
2.不能用於實時讀的場景.由於複製和add操作等需要時間,故讀取時可能讀到舊值。
能做到最終一致性,但無法滿足實時性的要求,更適合讀多寫少的場景。
如果無法知道數組有多大,或者add,set操作有多少,慎用此類,在大量的複製副本的過程中很容易出錯。設計思想:
1.讀寫分離
2.最終一致性
3.使用時另外開闢空間,防止併發衝突源碼分析
//構造方法
public CopyOnWriteArrayList(Collection<? extends E> c) {
Object[] elements;//使用對象數組來承載數據
if (c.getClass() == CopyOnWriteArrayList.class)
elements = ((CopyOnWriteArrayList<?>)c).getArray();
else {
elements = c.toArray();
// c.toArray might (incorrectly) not return Object[] (see 6260652)
if (elements.getClass() != Object[].class)
elements = Arrays.copyOf(elements, elements.length, Object[].class);
}
setArray(elements);
}
//添加數據方法
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);//複製當前數組並且擴容+1
newElements[len] = e;//將要添加的數據放入新數組
setArray(newElements);//將原來的數組指向新的數組
return true;
} finally {
lock.unlock();
}
}
//獲取數據方法,與普通的get沒什麼差別
private E get(Object[] a, int index) {
return (E) a[index];
}
HashSet –> CopyOnWriteArraySet
- 它是線程安全的,底層實現使用的是CopyOnWriteArrayList,因此它也適用於大小很小的set集合,只讀操作遠大於可變操作。因爲他需要copy整個數組,所以包括add、remove、set它的開銷相對於大一些。
- 迭代器不支持可變的remove操作。使用迭代器遍歷的時候速度很快,而且不會與其他線程發生衝突。
- 源碼分析:
//構造方法
public CopyOnWriteArraySet() {
al = new CopyOnWriteArrayList<E>();//底層使用CopyOnWriteArrayList
}
//添加元素方法,基本實現原理與CopyOnWriteArrayList相同
private boolean addIfAbsent(E e, Object[] snapshot) {
final ReentrantLock lock = this.lock;
lock.lock();
try {
Object[] current = getArray();
int len = current.length;
if (snapshot != current) {//添加了元素去重操作
// Optimize for lost race to another addXXX operation
int common = Math.min(snapshot.length, len);
for (int i = 0; i < common; i++)
if (current[i] != snapshot[i] && eq(e, current[i]))
return false;
if (indexOf(e, current, common, len) >= 0)
return false;
}
Object[] newElements = Arrays.copyOf(current, len + 1);
newElements[len] = e;
setArray(newElements);
return true;
} finally {
lock.unlock();
}
}
TreeSet –> ConcurrentSkipListSet
它是JDK6新增的類,同TreeSet一樣支持自然排序,並且可以在構造的時候自己定義比較器。
- 同其他set集合,是基於map集合的(基於ConcurrentSkipListMap),在多線程環境下,裏面的contains、add、remove操作都是線程安全的。
- 多個線程可以安全的併發的執行插入、移除、和訪問操作。但是對於批量操作addAll、removeAll、retainAll和containsAll並不能保證以原子方式執行,原因是addAll、removeAll、retainAll底層調用的還是contains、add、remove方法,只能保證每一次的執行是原子性的,代表在單一執行操縱時不會被打斷,但是不能保證每一次批量操作都不會被打斷。在使用批量操作時,還是需要手動加上同步操作的。
- 不允許使用null元素的,它無法可靠的將參數及返回值與不存在的元素區分開來。
- 源碼分析:
//構造方法
public ConcurrentSkipListSet() {
m = new ConcurrentSkipListMap<E,Object>();//使用ConcurrentSkipListMap實現
}
HashMap –> ConcurrentHashMap
- 不允許空值,在實際的應用中除了少數的插入操作和刪除操作外,絕大多數我們使用map都是讀取操作。而且讀操作大多數都是成功的。基於這個前提,它針對讀操作做了大量的優化。因此這個類在高併發環境下有特別好的表現。
- ConcurrentHashMap作爲Concurrent一族,其有着高效地併發操作,相比Hashtable的笨重,ConcurrentHashMap則更勝一籌了。
- 在1.8版本以前,ConcurrentHashMap採用分段鎖的概念,使鎖更加細化,但是1.8已經改變了這種思路,而是利用CAS+Synchronized來保證併發更新的安全,當然底層採用數組+鏈表+紅黑樹的存儲結構。
- 源碼分析:推薦參考chenssy的博文:J.U.C之Java併發容器:ConcurrentHashMap
TreeMap –> ConcurrentSkipListMap
- 底層實現採用SkipList跳錶
- 曾經有人用ConcurrentHashMap與ConcurrentSkipListMap做性能測試,在4個線程1.6W的數據條件下,前者的數據存取速度是後者的4倍左右。但是後者有幾個前者不能比擬的優點:
1、Key是有序的
2、支持更高的併發,存儲時間與線程數無關
安全共享對象策略
- 線程限制:一個被線程限制的對象,由線程獨佔,並且只能被佔有它的線程修改
- 共享只讀:一個共享只讀的U帝鄉,在沒有額外同步的情況下,可以被多個線程併發訪問,但是任何線程都不能修改它
- 線程安全對象:一個線程安全的對象或者容器,在內部通過同步機制來保障線程安全,多以其他線程無需額外的同步就可以通過公共接口隨意訪問他
- 被守護對象:被守護對象只能通過獲取特定的鎖來訪問。