(接上文《源碼閱讀(28):Java中線程安全的List結構——CopyOnWriteArrayList(1)》)
4、CopyOnWriteArrayList的主要方法
當完成CopyOnWriteArrayList集合的初始化過程的介紹後,本文再列舉幾個該集合典型的方法,以便幫助讀者理解該集合是如果基於一個內存副本完成寫操作的,以及這樣做的有點和缺點。
4.1、get(int)方法
get(int)方法是從CopyOnWriteArrayList集合中獲取指定索引位置上元素對象的方法,該方法無需保證線程安全性,任務操作者、任何線程、任何時間點都可以通過該方法或類似方法獲取CopyOnWriteArrayList集合中的數據,究其根本原因,就是因爲該集合的所有寫操作都是在一個內存副本中進行,所以任何讀性質的操作都不會受影響:
public E get(int index) {
return get(getArray(), index);
}
/**
* Gets the array. Non-private so as to also be accessible
* from CopyOnWriteArraySet class.
*/
final Object[] getArray() {
return array;
}
@SuppressWarnings("unchecked")
private E get(Object[] a, int index) {
return (E) a[index];
}
代碼很簡單,以至於無非進行任何說明。這種數據讀取方式因爲不需要考慮任何鎖機制,並且數組可以支持隨機位置上的讀操作,所以其時間複雜度任何時候都爲O(1)。
4.2、add(E)方法
使用add方法,向CopyOnWriteArrayList集合的最後一個數組索引位添加一個新的元素(引用),添加的元素可以爲null。源代碼片段如下所示:
public boolean add(E e) {
final ReentrantLock lock = this.lock;
// 通過lock所對應獲取操作以下代碼的操作權
lock.lock();
try {
// 獲取到當前集合使用的數組對象
Object[] elements = getArray();
// 獲取當前集合的元素大小
int len = elements.length;
// 使用Arrays.copy方法創建一個內存副本newElements數組
// 注意,副本數組的容量比當前CopyOnWriteArrayList集合的容量大1
Object[] newElements = Arrays.copyOf(elements, len + 1);
// 在newElements數組的最後一個索引位新增這個元素(引用)
newElements[len] = e;
// 設置完成後,最後將當前使用數組替換成副本,是副本數組成爲CopyOnWriteArrayList集合的內部數組
setArray(newElements);
return true;
} finally {
// 最後釋放操作權
lock.unlock();
}
}
這裏需要注意一個細節:在add方法所有處理邏輯開始前,先進行CopyOnWriteArrayList集合的操作權獲取,它並不影響CopyOnWriteArrayList集合的讀操作,因爲通過上一小節中介紹get方法的源代碼內容可知,CopyOnWriteArrayList集合的讀操作完全無視鎖權限,也不會有多線程下的數據操作問題。之所以類似add方法這樣的CopyOnWriteArrayList容器寫操作方法需要獲取操作權,主要是爲了防止其它線程可能對CopyOnWriteArrayList集合同時進行的寫操作造成數據錯誤。
從以上add方法的詳細描述我們可以知道,該集合通過Arrays.copyOf方法(其內部是System.arraycopy方法)創建一個新的內存區域,存放數組副本,並在副本上進行寫操作,最後將CopyOnWriteArrayList集合中的數組引用爲副本數組。
4.3、set(int , E)方法
set方法用於替換CopyOnWriteArrayList集合指定數組索引位上的元素。該方法的操作過程和add方法類似,源代碼如下所示:
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;
setArray(newElements);
}
// 如果原始對象和將要重新設定的對象相等(依據內存地址)
// 從理論上講,無需對CopyOnWriteArrayList集合的當前數組重新進行設定,
// 但這裏還是重新設定了一次
else {
// Not quite a no-op; ensures volatile write semantics
setArray(elements);
}
// 處理完成後,該索引爲上原始的數據對象將會被返回
return oldValue;
} finally {
lock.unlock();
}
}
請注意源代碼中的一句註釋:“Not quite a no-op; ensures volatile write semantics”,當指定數組索引位上的原始數據對象和將要新替換的數據對象“相等”時,從理論上講實際上就不需要創建副本進行寫操作,也不再需要通過setArray方法進行數組替換操作了。
但從以上源代碼中我們可以看到當以上場景出現時,源代碼仍然調用了setArray方法進行數組的設置操作,爲什麼會這樣呢?這主要是爲了保證外部調用者的非volatile變量遵循happen−before原則。該原則涉及到JMM(java內存模型)和指令重排的知識點,有興趣的讀者可自行查詢資料。
5、Collections.synchronizedList的補充作用
5.1、CopyOnWriteArrayList工作機制的優缺點
從以上關於CopyOnWriteArrayList集合的工作介紹中,我們可以大致歸納出CopyOnWriteArrayList集合的特點和它的一些優缺點:
-
該集合適合應用在多線程並行場景下,如果讀者使用集合的場景中不涉及多線程操作,則不建議使用該集合。甚至不建議使用java.util.concurrent包下的任何集合類——使用java.util包中的基本java集合框架即可。
-
該集合在多線程併發操作的場景下,主要的關注點集中在於如何保證集合的線程安全性,和集合的數據讀操作性能。爲此,該集合以顯著犧牲自身的寫操作性能和內存空間的方式,來換取讀操作性能不受影響。這個特徵很好理解,每次進行讀操作前都要創建一個內存副本,這種操作一定會對內存空間造成浪費,且內存複製操作一定會造成多餘的性能消耗。
-
所以這種集合適用於多線程併發操作場景下,那些多線程讀操作遠遠大於寫操作次數,且集合中存儲的數據規模不大的場景。
5.2、Collections.synchronizedList對CopyOnWriteArrayList的補充
那麼Java原生類、工具包中,有沒有提供一些適合在多線程併發操作場景下使用的,其讀操作性能和寫操作性能保持一定平衡性的,雖然整體性能不是最好但依然保證線程安全的,最後又是List性質的集合呢?答案是:有的。
java.util.Collections是Java爲開發人員提供的一個和集合操作相關的工具包(JDK1.2便開始提供,各版本又做了不同程度的代碼調整),其中提供了一組方法,可以將java.util包下的那些線程不安全的集合轉變爲線程安全的集合。實際上就是使用java object monitor機制,將集合方法進行了封裝。請看如下示例:
// ......
// 通過Collections提供的synchronizedList方法
// 將線程不安全的ArrayList封裝爲線程安全的List集合
List<String> syncList = java.util.Collections.synchronizedList(new ArrayList<>());
// 對於使用者來說,集合的增刪改查功能不受影響
syncList.add("a");
syncList.add("b");
syncList.add("c");
syncList.add("d");
syncList.add("e");
// ......
// 通過Collections提供的synchronizedList方法
// 將線程不安全的TreeSet集合封裝爲線程安全的Set集合
Set<String> syncSet = java.util.Collections.synchronizedSortedSet(new TreeSet<>());
syncSet.add("a");
syncSet.add("b");
syncSet.add("c");
syncSet.add("d");
syncSet.add("e");
// ......
但是,使用經過這個工具封裝的集合需要特別注意一點,就是原始集合的迭代器(iterator)、可拆分的迭代器(spliterator)、處理流(stream)、並行流(parallelStream)的運行都不受線程安全的封裝保護,如果用戶需要這樣的集合使用方式,則必須自行控制線程安全。
另外,由於java.util.Collections.synchronizedXXXX這樣的線程安全集合封裝方式,其內部使用的是java object monitor這種鎖機制,所以它也不適合在併發量非常高的場景中使用。最後我們基於java.util.Collections.synchronizedList方法,簡述一下其內部的工作原理:
public class Collections {
// Suppresses default constructor, ensuring non-instantiability.
private Collections() {
}
// ......
// 其中包括了Collection接口中各方法的實現
static class SynchronizedCollection<E> implements Collection<E>, Serializable {
// Backing Collection
// 被封裝的真實集合,由該屬性記錄(引用)
final Collection<E> c;
// Object on which to synchronize
// 整個集合線程安全性封裝的機制中,使用該對象管理Object Monitor鎖機制
final Object mutex;
// ......
// 以下諸如size、add這樣的集合操作方法,全部基於mutex對象,基於Objct Monitor進行封裝
public int size() {
synchronized (mutex) {return c.size();}
}
public boolean add(E e) {
synchronized (mutex) {return c.add(e);}
}
// ......
// 以下這些方法沒有進行線程安全封裝,需要使用者手動控制
// Must be manually synched by user!
public Iterator<E> iterator() {
return c.iterator();
}
// Must be manually synched by user!
@Override
public Spliterator<E> spliterator() {
return c.spliterator();
}
// Must be manually synched by user!
@Override
public Stream<E> stream() {
return c.stream();
}
// Must be manually synched by user!
@Override
public Stream<E> parallelStream() {
return c.parallelStream();
}
}
// ......
// 該類繼承於SynchronizedCollection
static class SynchronizedList<E> extends SynchronizedCollection<E> implements List<E> {
// 被封裝的真實List集合,由該屬性記錄(引用)
final List<E> list;
// 並且通過構造函數的重寫,將父類中c屬性賦值爲當前的list屬性
SynchronizedList(List<E> list) {
super(list);
this.list = list;
}
SynchronizedList(List<E> list, Object mutex) {
super(list, mutex);
this.list = list;
}
// ......
// 諸如以下由list接口定義的操作方法,也被重新封裝
public E get(int index) {
synchronized (mutex) {return list.get(index);}
}
public E set(int index, E element) {
synchronized (mutex) {return list.set(index, element);}
}
public void add(int index, E element) {
synchronized (mutex) {list.add(index, element);}
}
}
// ......
// 該類繼承於SynchronizedList,當封裝的List性質的集合支持RandomAccess隨機訪問
// 就使用該類進行線程安全性封裝
static class SynchronizedRandomAccessList<E> extends SynchronizedList<E> implements RandomAccess {
public List<E> subList(int fromIndex, int toIndex) {
synchronized (mutex) {
return new SynchronizedRandomAccessList<>(list.subList(fromIndex, toIndex), mutex);
}
}
}
}
以上源代碼展示了SynchronizedRandomAccessList、SynchronizedList和SynchronizedCollection這三個工具類的繼承關係,以及它們三者是如何配合完成集合線程安全性封裝控制的,如下圖所示:
現在我們來看一下當調用java.util.Collections.synchronizedList方法時發生了什麼事情:
public static <T> List<T> synchronizedList(List<T> list) {
// 如果當前List集合支持RandomAccess隨機訪問,則使用SynchronizedRandomAccessList對集合進行線程安全性封裝
// 否則使用SynchronizedList對集合進行線程安全性封裝
return (list instanceof RandomAccess ? new SynchronizedRandomAccessList<>(list) : new SynchronizedList<>(list));
}