本小節是《併發容器》的最後一部分,這一個小節描述的是針對List/Set接口的一個線程版本。html
在《併發隊列與Queue簡介》中介紹了併發容器的一個歸納,主要描述的是Queue的實現。其中特別提到一點LinkedList是List/Queue的實現,可是LinkedList確實非線程安全的。無論BlockingQueue仍是ConcurrentMap的實現,咱們發現都是針對鏈表的實現,固然儘量的使用CAS或者Lock的特性,同時都有經過鎖部分容器來提供併發的特性。而對於List或者Set而言,增、刪操做其實都是針對整個容器,所以每次操做都不可避免的須要鎖定整個容器空間,性能確定會大打折扣。要實現一個線程安全的List/Set,只須要在修改操做的時候進行同步便可,好比使用java.util.Collections.synchronizedList(List<T>)或者java.util.Collections.synchronizedSet(Set<T>)。固然也可使用Lock來實現線程安全的List/Set。java
一般狀況下咱們的高併發都發生在「多讀少寫」的狀況,所以若是可以實現一種更優秀的算法這對生產環境仍是頗有好處的。ReadWriteLock固然是一種實現。CopyOnWriteArrayList/CopyOnWriteArraySet確實另一種思路。算法
CopyOnWriteArrayList/CopyOnWriteArraySet的基本思想是一旦對容器有修改,那麼就「複製」一份新的集合,在新的集合上修改,而後將新集合複製給舊的引用。固然了這部分少不了要加鎖。顯然對於CopyOnWriteArrayList/CopyOnWriteArraySet來講最大的好處就是「讀」操做不須要鎖了。數組
咱們來看看源碼。安全
/** The array, accessed only via getArray/setArray. */
private volatile transient Object[] array;
public E get(int index) {
return (E)(getArray()[index]);
}
private static int indexOf(Object o, Object[] elements,
int index, int fence) {
if (o == null) {
for (int i = index; i < fence; i++)
if (elements[i] == null)
return i;
} else {
for (int i = index; i < fence; i++)
if (o.equals(elements[i]))
return i;
}
return -1;
}
public Iterator<E> iterator() {
return new COWIterator<E>(getArray(), 0);
}
public void clear() {
final ReentrantLock lock = this.lock;
lock.lock();
try {
setArray(new Object[0]);
} finally {
lock.unlock();
}
}
對於上述代碼,有幾點說明:併發
- List仍然是基於數組的實現,由於只有數組是最快的。
- 爲了保證無鎖的讀操做可以看到寫操做的變化,所以數組array是volatile類型的。
- get/indexOf/iterator等操做都是無鎖的,同時也能夠看到所操做的都是某一時刻array的鏡像(這得益於數組是不可變化的)
- add/set/remove/clear等元素變化的都是須要加鎖的,這裏使用的是ReentrantLock。
這裏有一段有意思的代碼片斷。高併發
public E set(int index, E element) {
final ReentrantLock lock = this.lock;
lock.lock();
try {
Object[] elements = getArray();
Object oldValue = elements[index];
if (oldValue != element) {
int len = elements.length;
Object[] newElements = Arrays.copyOf(elements, len);
newElements[index] = element;
setArray(newElements);
} else {
// Not quite a no-op; ensures volatile write semantics
setArray(elements);
}
return (E)oldValue;
} finally {
lock.unlock();
}
}
final void setArray(Object[] a) {
array = a;
}
對於set操做,若是元素有變化,修改後setArray(newElements);將新數組賦值還好理解。那麼若是一個元素沒有變化,也就是上述代碼的else部分,爲何還須要進行一個無謂的setArray操做?畢竟setArray操做沒有改變任何數據。性能
對於這個問題也是頗有意思,有一封郵件討論了此問題(1、2、3)。
大體的意思是,儘管沒有改變任何數據,可是爲了保持「volatile」的語義,任何一個讀操做都應該是一個寫操做的結果,也就是讀操做看到的數據必定是某個寫操做的結果(儘管寫操做沒有改變數據自己)。因此這裏即便不設置也沒有問題,僅僅是爲了一個語義上的補充(我的理解)。ui
這裏還有一個有意思的討論,說什麼addIfAbsent在元素沒有變化的時候爲何沒有setArray操做?這個要看怎麼理解addIfAbsent的語義了。若是說addIfAbsent語義是」寫「或者」不寫「操做,而把」不寫「操做看成一次」讀「操做的話,那麼」讀「操做就不須要保持volatile語義了。this
對於CopyOnWriteArraySet而言就簡單多了,只是持有一個CopyOnWriteArrayList,僅僅在add/addAll的時候檢測元素是否存在,若是存在就不加入集合中。
private final CopyOnWriteArrayList<E> al;
/**
* Creates an empty set.
*/
public CopyOnWriteArraySet() {
al = new CopyOnWriteArrayList<E>();
}
public boolean add(E e) {
return al.addIfAbsent(e);
}
在使用上CopyOnWriteArrayList/CopyOnWriteArraySet就簡單多了,和List/Set基本相同,這裏就再也不介紹了。
整個併發容器結束了,接下來好好規劃下線程池部分,而後進入最後一部分的梳理。