我在前面總結了Java集合中ArrayList的源碼細節,其中也提到了ArrayList是線程不安全的,那麼jdk中爲咱們提供的線程安全的List是什麼呢,就是下面要說的CopyOnWriteList這個併發安全的集合類,它主要採用的就是copy-on-write
思想,這個思想大概就是讀寫分離:讀時共享、寫時複製(本來的array)更新(且爲獨佔式的加鎖)
,而咱們下面分析的源碼具體實現也是這個思想的體現java
仍是先貼上CopyOnWriteList的繼承體系吧,能夠看到其實現了Serializable、Cloneable和RandomAccess接口,具備隨機訪問的特色,實現了List接口,具有List的特性。數組
咱們單獨看一下CopyOnWriteList的主要屬性和下面要主要分析的方法有哪些。從圖中看出:安全
每一個CopyOnWriteList對象裏面有一個array數組來存放具體元素多線程
使用ReentrantLock獨佔鎖來保證只有寫線程對array副本進行更新。關於ReentrantLock能夠參考我另外一篇AQS的應用之ReentrantLock。併發
CopyOnWriteArrayList在遍歷的使用不會拋出ConcurrentModificationException異常,而且遍歷的時候就不用額外加鎖app
下面仍是主要看CopyOnWriteList的實現dom
//這個就是保證更新數組的時候只有一個線程可以獲取lock,而後更新
final transient ReentrantLock lock = new ReentrantLock();
//使用volatile修飾的array,保證寫線程更新array以後別的線程可以看到更新後的array.
//可是並不能保證明時性:在數組副本上添加元素以後,尚未更新array指向新地址以前,別的讀線程看到的仍是舊的array
private transient volatile Object[] array;
//獲取數組,非private的,final修飾
final Object[] getArray() {
return array;
}
//設置數組
final void setArray(Object[] a) {
array = a;
}
複製代碼
(1)無參構造,默認建立的是一個長度爲0的數組ide
//這裏就是構造方法,建立一個新的長度爲0的Object數組
//而後調用setArray方法將其設置給CopyOnWriteList的成員變量array
public CopyOnWriteArrayList() {
setArray(new Object[0]);
}
複製代碼
(2)參數爲Collection的構造方法post
//按照集合的迭代器遍歷返回的順序,建立包含傳入的collection集合的元素的列表
//若是傳遞的參數爲null,會拋出異常
public CopyOnWriteArrayList(Collection<? extends E> c) {
Object[] elements; //一個elements數組
//這裏是判斷傳遞的是否就是一個CopyOnWriteArrayList集合
if (c.getClass() == CopyOnWriteArrayList.class)
//若是是,直接調用getArray方法,得到傳入集合的array而後賦值給elements
elements = ((CopyOnWriteArrayList<?>)c).getArray();
else {
//先將傳入的集合轉變爲數組形式
elements = c.toArray();
//c.toArray()可能不會正確地返回一個 Object[]數組,那麼使用Arrays.copyOf()方法
if (elements.getClass() != Object[].class)
elements = Arrays.copyOf(elements, elements.length, Object[].class);
}
//直接調用setArray方法設置array屬性
setArray(elements);
}
複製代碼
(3)建立一個包含給定數組副本的listui
public CopyOnWriteArrayList(E[] toCopyIn) {
setArray(Arrays.copyOf(toCopyIn, toCopyIn.length, Object[].class));
}
複製代碼
上面介紹的是CopyOnWriteList的初始化,三個構造方法都比較易懂,後面仍是主要看看幾個主要方法的實現
下面是add(E e)方法的實現 ,以及詳細註釋
public boolean add(E e) {
//得到獨佔鎖
final ReentrantLock lock = this.lock;
//加鎖
lock.lock();
try {
//得到list底層的數組array
Object[] elements = getArray();
//得到數組長度
int len = elements.length;
//拷貝到新數組,新數組長度爲len+1
Object[] newElements = Arrays.copyOf(elements, len + 1);
//給新數組末尾元素賦值
newElements[len] = e;
//用新的數組替換掉原來的數組
setArray(newElements);
return true;
} finally {
lock.unlock();//釋放鎖
}
}
複製代碼
總結一下add方法的執行流程
因此總結起來就是,多線程下只有一個線程可以獲取到鎖,而後使用複製原有數組的方式添加元素,以後再將新的數組替換原有的數組,最後釋放鎖(別的add線程去執行)。
最後還有一點就是,數組長度不是固定的,每次寫以後數組長度會+1,因此CopyOnWriteList也沒有length或者size這類屬性,可是提供了size()方法,獲取集合的實際大小,size()方法以下
public int size() {
return getArray().length;
}
複製代碼
使用get(i)能夠獲取指定位置i的元素,固然若是元素不存在就會拋出數組越界異常。
public E get(int index) {
return get(getArray(), index);
}
final Object[] getArray() {
return array;
}
private E get(Object[] a, int index) {
return (E) a[index];
}
複製代碼
固然get方法這裏也體現了copy-on-write-list
的弱一致性問題。咱們用下面的圖示簡略說明一下。圖中給的假設狀況是:threadA訪問index=1處的元素
由於咱們看到get過程是沒有加鎖的(假設array中有三個元素如圖所示)。假設threadA執行①以後②以前,threadB執行remove(1)操做,threadB或獲取獨佔鎖,而後執行寫時複製操做,即複製一個新的數組neArray
,而後在newArray中執行remove操做(1),更新array。threadB執行完畢array中index=1的元素已是item3了。
而後threadA繼續執行,可是由於threadA操做的是原數組中的元素,這個時候的index=1仍是item2。因此最終現象就是雖然threadB刪除了位置爲1處的元素,可是threadA仍是訪問的原數組的元素。這就是若一致性問題
修改也是屬於寫
,因此須要獲取lock,下面就是set方法的實現
public E set(int index, E element) {
//獲取鎖
final ReentrantLock lock = this.lock;
//進行加鎖
lock.lock();
try {
//獲取數組array
Object[] elements = getArray();
//獲取index位置的元素
E oldValue = get(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
//爲了保證volatile 語義,即便沒有修改,也要替換成新的數組
setArray(elements);
}
return oldValue; //返回舊值
} finally {
lock.unlock();//釋放鎖
}
}
複製代碼
看了set方法以後,發現其實和add方法實現相似。
下面是remove方法的實現,總結就是
public E remove(int index) {
//獲取鎖
final ReentrantLock lock = this.lock;
//加鎖
lock.lock();
try {
//獲取原數組
Object[] elements = getArray();
//獲取原數組長度
int len = elements.length;
//獲取原數組index處的值
E oldValue = get(elements, index);
//由於數組刪除元素須要移動,因此這裏就是計算須要移動的個數
int numMoved = len - index - 1;
//計算的numMoved=0,表示要刪除的是最後一個元素,
//那麼舊直接將原數組的前len-1個複製到新數組中,替換舊數組便可
if (numMoved == 0)
setArray(Arrays.copyOf(elements, len - 1));
//要刪除的不是最後一個元素
else {
//建立一個長度爲len-1的數組
Object[] newElements = new Object[len - 1];
//將原數組中index以前的元素複製到新數組
System.arraycopy(elements, 0, newElements, 0, index);
//將原數組中index以後的元素複製到新數組
System.arraycopy(elements, index + 1, newElements, index,
numMoved);
//用新數組替換原數組
setArray(newElements);
}
return oldValue;//返回舊值
} finally {
lock.unlock();//釋放鎖
}
}
複製代碼
迭代器的基本使用方式以下,hashNext()方法用來判斷是否還有元素,next方法返回具體的元素。
CopyOnWriteArrayList list = new CopyOnWriteArrayList();
Iterator<?> itr = list.iterator();
while(itr.hashNext()) {
//do sth
itr.next();
}
複製代碼
那麼在CopyOnWriteArrayList中的迭代器是怎樣實現的呢,爲何說是弱一致性呢(先獲取迭代器的,可是若是在獲取迭代器以後別的線程對list進行了修改,這對於迭代器是不可見的
),下面就說一下CopyOnWriteArrayList中的實現
//Iterator<?> itr = list.iterator();
public Iterator<E> iterator() {
//這裏能夠看到,是先獲取到原數組getArray(),這裏記爲oldArray
//而後調用COWIterator構造器將oldArray做爲參數,建立一個迭代器對象
//從下面的COWIterator類中也能看到,其中有一個成員存儲的就是oldArray的副本
return new COWIterator<E>(getArray(), 0);
}
static final class COWIterator<E> implements ListIterator<E> {
//array的快照版本
private final Object[] snapshot;
//後續調用next返回的元素索引(數組下標)
private int cursor;
//構造器
private COWIterator(Object[] elements, int initialCursor) {
cursor = initialCursor;
snapshot = elements;
}
//變量是否結束:下標小於數組長度
public boolean hasNext() {
return cursor < snapshot.length;
}
//是否有前驅元素
public boolean hasPrevious() {
return cursor > 0;
}
//獲取元素
//hasNext()返回true,直接經過cursor記錄的下標獲取值
//hasNext()返回false,拋出異常
public E next() {
if (! hasNext())
throw new NoSuchElementException();
return (E) snapshot[cursor++];
}
//other method...
}
複製代碼
在上面的代碼中咱們能看處,list的iterator()方法實際上返回的是一個COWIterator對象,COWIterator對象的snapshot成員變量保存了當前
list中array存儲的內容,可是snapshot能夠說是這個array的一個快照,爲何這樣說呢
咱們傳遞的是雖然是當前的
array
,可是可能有別的線程對array
進行了修改而後將本來的array
替換掉了,那麼這個時候list中的array
和snapshot
引用的array
就不是一個了,做爲原array
的快照存在,那麼迭代器訪問的也就不是更新後的數組了。這就是弱一致性的體現
咱們看下面的例子
public class TestCOW {
private static CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList();
public static void main(String[] args) throws InterruptedException {
list.add("item1");
list.add("item2");
list.add("item3");
Thread thread = new Thread() {
@Override
public void run() {
list.set(1, "modify-item1");
list.remove("item2");
}
};
//main線程先得到迭代器
Iterator<String> itr = list.iterator();
thread.start();//啓動thread線程
thread.join();//這裏讓main線程等待thread線程執行完,而後再遍歷看看輸出的結果是否是修改後的結果
while (itr.hasNext()) {
System.out.println(Thread.currentThread().getName() + "線程中的list的元素:" + itr.next());
}
}
}
複製代碼
運行結果以下。實際上再上面的程序中咱們先向list中添加了幾個元素,而後再thread中修改list,同時讓main線程先得到list的迭代器
,並等待thread執行完而後打印list中的元素,發現 main線程並無發現list中的array的變化,輸出的仍是原來的list,這就是弱一致性的體現。
main線程中的list的元素:item1 main線程中的list的元素:item2 main線程中的list的元素:item3
寫
時線程安全的:使用ReentrantLock獨佔鎖,保證同時只有一個線程對集合進行寫
操做寫
操做會更新array) 注意到set方法中有一段代碼是這樣的
else { //oldValue = element(element是傳入的參數)
// Not quite a no-op; ensures volatile write semantics
//爲了保證volatile 語義,即便沒有修改,也要替換成新的數組
setArray(elements);
}
複製代碼
其實就是說要指定位置要修改的值和數組中那個位置的值是相同的,可是仍是須要調用set方法更新array,這是爲何呢,參考這個帖子,總結就是爲了維護happens-before原則
。首先看一下這段話
java.util.concurrent 中全部類的方法及其子包擴展了這些對更高級別同步的保證。尤爲是: 線程中將一個對象放入任何併發 collection 以前的操做 happen-before 從另外一線程中的 collection 訪問或移除該元素的**
後續操做
**。
能夠理解爲這裏是爲了保證set操做以前的系列操做happen-before與別的線程訪問array(不加鎖)的後續操做
,參照下面的例子
// 這是兩個線程的初始狀況
int nonVolatileField = 0; //一個不被volatile修飾的變量
//僞代碼
CopyOnWriteArrayList<String> list = {"x","y","z"}
// Thread 1
// (1)這裏更新了nonVolatileField
nonVolatileField = 1;
// (2)這裏是set()修改(寫)操做,注意這裏會對volatile修飾的array進行寫操做
list.set(0, "x");
// Thread 2
// (3)這裏是訪問(讀)操做
String s = list.get(0);
// (4)使用nonVolatileField
if (s == "x") {
int localVar = nonVolatileField;
}
複製代碼
假設存在以上場景,若是能保證只會存在這樣的軌跡:(1)->(2)->(3)->(4).根據上述java API文檔中的約定有
(2)happen-before與(3),在線程內的操做有(1)happen-before與(2),(3)happen-before與(4),根據happen-before的傳遞性讀寫nonVolatileField變量就有(1)happen-before與(4)
因此Thread 1對nonVolatileField的寫操做對Thread 2中a的讀操做可見。若是CopyOnWriteArrayList的set的else裏沒有setArray(elements)
對volatile變量的寫
的話,(2)happen-before與(3)就再也不有了,上述的可見性也就沒法保證。
因此就是爲了保證set操做以前的系列操做happen-before與別的線程訪問array(不加鎖)的後續操做
,