https://mp.weixin.qq.com/s/nvOhOPp9_wQXP6sTcjuqlQ java
相信你們對 ConcurrentHashMap 這個線程安全類很是熟悉,可是若是我想在多線程環境下使用 ArrayList,該怎麼處理呢?阿粉今天來給你揭曉答案!數組
在介紹 CopyOnWriteArrayList 以前,咱們一塊兒先來看看以下方法執行結果,代碼內容以下:安全
public static void main(String[] args) {
List<String> list = new ArrayList<String>();
list.add("1");
list.add("2");
list.add("1");
System.out.println("原始list元素:"+ list.toString());
//經過對象移除等於內容爲1的元素
for (String item : list) {
if("1".equals(item)) {
list.remove(item);
}
}
System.out.println("經過對象移除後的list元素:"+ list.toString());
}
執行結果內容以下:多線程
原始list元素:[1, 2, 1]
Exception in thread "main" java.util.ConcurrentModificationException
at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:909)
at java.util.ArrayList$Itr.next(ArrayList.java:859)
at com.example.container.a.TestList.main(TestList.java:16)
很遺憾,結果並無達到咱們想要的預期效果,執行以後直接報錯!拋ConcurrentModificationException異常!併發
爲啥會拋這個異常呢?ide
咱們一塊兒來看看,foreach
寫法其實是對List.iterator()
迭代器的一種簡寫,所以咱們能夠從分析List.iterator()
迭代器進行入手,看看爲啥會拋這個異常。性能
ArrayList
類中的Iterator
迭代器實現,源碼內容:測試
經過代碼咱們發現 Itr
是 ArrayList
中定義的一個私有內部類,每次調用next
、remove
方法時,都會調用checkForComodification
方法,源碼以下:spa
/**修改次數檢查*/
final void checkForComodification() {
//檢查List中的修改次數是否與迭代器類中的修改次數相等
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
checkForComodification
方法,其實是用來檢查List
中的修改次數modCount
是否與迭代器類中的修改次數expectedModCount
相等,若是不相等,就會拋出ConcurrentModificationException
異常!線程
那麼問題基本上已經清晰了,上面的運行結果之因此會拋出這個異常,就是由於List
中的修改次數modCount
與迭代器類中的修改次數expectedModCount
不相同形成的!
閱讀過集合源碼的朋友,可能想起Vector
這個類,它不是 JDK 中 ArrayList 線程安全的一個版本麼?
好的,爲了眼見爲實,咱們把ArrayList
換成Vector
來測試一下,代碼以下:
public static void main(String[] args) {
Vector<String> list = new Vector<String>();
//模擬10個線程向list中添加內容,而且讀取內容
for (int i = 0; i < 5; i++) {
final int j = i;
new Thread(new Runnable() {
@Override
public void run() {
//添加內容
list.add(j + "-j");
//讀取內容
for (String str : list) {
System.out.println("內容:" + str);
}
}
}).start();
}
}
執行程序,運行結果以下:
仍是同樣的結果,拋異常了,Vector
雖然線程安全,只不過是加了synchronized
關鍵字,可是迭代問題徹底沒有解決!
繼續回到本文要介紹的 CopyOnWriteArrayList 類,咱們把上面的例子,換成CopyOnWriteArrayList
類來試試,源碼內容以下:
public static void main(String[] args) {
//將ArrayList換成CopyOnWriteArrayList
CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();
list.add("1");
list.add("2");
list.add("1");
System.out.println("原始list元素:"+ list.toString());
//經過對象移除等於11的元素
for (String item : list) {
if("1".equals(item)) {
list.remove(item);
}
}
System.out.println("經過對象移除後的list元素:"+ list.toString());
}
執行結果以下:
原始list元素:[1, 2, 1]
經過對象移除後的list元素:[2]
呃呵,執行成功了,沒有報錯!是否是很神奇~~
固然,相似上面這樣的例子有不少,好比寫10個線程向list
中添加元素讀取內容,也會拋出上面那個異常,操做以下:
public static void main(String[] args) {
final List<String> list = new ArrayList<>();
//模擬10個線程向list中添加內容,而且讀取內容
for (int i = 0; i < 10; i++) {
final int j = i;
new Thread(new Runnable() {
@Override
public void run() {
//添加內容
list.add(j + "-j");
//讀取內容
for (String str : list) {
System.out.println("內容:" + str);
}
}
}).start();
}
}
相似的操做例子就很是多了,這裏就不一一舉例了。
CopyOnWriteArrayList 其實是 ArrayList 一個線程安全的操做類!
從它的名字能夠看出,CopyOnWrite
是在寫入的時候,不修改原內容,而是將原來的內容複製一份到新的數組,而後向新數組寫完數據以後,再移動內存指針,將目標指向最新的位置。
從 JDK1.5 開始 Java 併發包裏提供了兩個使用CopyOnWrite
機制實現的併發容器,分別是CopyOnWriteArrayList
和CopyOnWriteArraySet
。
從名字上看,CopyOnWriteArrayList
主要針對動態數組,一個線程安全版本的 ArrayList !
而CopyOnWriteArraySet
主要針對集,CopyOnWriteArraySet
能夠理解爲HashSet
線程安全的操做類,咱們都知道HashSet
基於散列表HashMap
實現,可是CopyOnWriteArraySet
並非基於散列表實現,而是基於CopyOnWriteArrayList
動態數組實現!
關於這一點,咱們能夠從它的源碼中得出結論,部分源碼內容:
從源碼上能夠看出,CopyOnWriteArraySet
默認初始化的時候,實例化了CopyOnWriteArrayList
類,CopyOnWriteArraySet
的大部分方法,例如add
、remove
等方法都基於CopyOnWriteArraySet
實現!
二者最大的不一樣點是,CopyOnWriteArrayList
能夠容許元素重複,而CopyOnWriteArraySet
不容許有重複的元素!
好了,繼續來 BB 本文要介紹的CopyOnWriteArrayList
類~~
打開CopyOnWriteArrayList
類的源碼,內容以下:
能夠看到 CopyOnWriteArrayList
的存儲元素的數組array
變量,使用了volatile
關鍵字保證的多線程下數據可見行;同時,使用了ReentrantLock
可重入鎖對象,保證線程操做安全。
在初始化階段,CopyOnWriteArrayList
默認給數組初始化了一個對象,固然,初始化方法還有不少,好比以下咱們常常會用到的一個初始化方法,源碼內容以下:
這個方法,表示若是咱們傳入的是一個 ArrayList
數組對象,會將對象內容複製一份到新的數組中,而後初始化進去,操做以下:
List<String> list = new ArrayList<>();
...
//CopyOnWriteArrayList將list內容複製出來,並建立一個新的數組
CopyOnWriteArrayList<String> copyList = new CopyOnWriteArrayList<>(list);
CopyOnWriteArrayList
是對原數組內容進行復制再寫入,那麼是否是也存在多線程下操做也會發生衝突呢?
下面咱們再一塊兒來看看它的方法實現!
add()
方法是CopyOnWriteArrayList
的添加元素的入口!
CopyOnWriteArrayList
之因此能保證多線程下安全操做, add()
方法功不可沒,源碼以下:
操做步驟以下:
在 Java 中,獨佔鎖方面,有2種方式能夠保證線程操做安全,一種是使用虛擬機提供的synchronized
來保證併發安全,另外一種是使用JUC
包下的ReentrantLock
可重入鎖來保證線程操做安全。
CopyOnWriteArrayList
使用了ReentrantLock
這種可重入鎖,保證了線程操做安全,同時數組變量array
使用volatile
保證多線程下數據的可見性!
其餘的,還有指定下標進行添加的方法,如add(int index, E element)
,操做相似,先找到須要添加的位置,若是是中間位置,則以添加位置爲分界點,分兩次進行復制,最後寫入數據!
remove()
方法是CopyOnWriteArrayList
的移除元素的入口!
源碼以下:
操做相似添加方法,步驟以下:
array
值;index
爲分界點,分兩節複製;固然,移除的方法還有基於對象的remove(Object o)
,原理也是同樣的,先找到元素的下標,而後執行移除操做。
get()
方法是CopyOnWriteArrayList
的查詢元素的入口!
源碼以下:
public E get(int index) {
//獲取數組內容,經過下標直接獲取
return get(getArray(), index);
}
查詢由於不涉及到數據操做,因此無需使用鎖進行處理!
上文中咱們介紹到,基本都是在遍歷元素的時候由於修改次數與迭代器中的修改次數不一致,致使檢查的時候拋異常,咱們一塊兒來看看CopyOnWriteArrayList
迭代器實現。
打開源碼,能夠得出CopyOnWriteArrayList
返回的迭代器是COWIterator
,源碼以下:
public Iterator<E> iterator() {
return new COWIterator<E>(getArray(), 0);
}
打開COWIterator
類,其實它是CopyOnWriteArrayList
的一個靜態內部類,源碼以下:
能夠看出,在使用迭代器的時候,遍歷的元素都來自於上面的getArray()
方法傳入的對象數組,也就是傳遞進來的 array 數組!
因而可知,CopyOnWriteArrayList 在使用迭代器遍歷的時候,操做的都是原數組,沒有像上面那樣進行修改次數判斷,因此不會拋異常!
固然,從源碼上也能夠得出,使用CopyOnWriteArrayList
的迭代器進行遍歷元素的時候,不能調用remove()
方法移除元素,由於不支持此操做!
若是想要移除元素,只能使用CopyOnWriteArrayList
提供的remove()
方法,而不是迭代器的remove()
方法,這個須要注意一下!
CopyOnWriteArrayList
是一個典型的讀寫分離的動態數組操做類!
在寫入數據的時候,將舊數組內容複製一份出來,而後向新的數組寫入數據,最後將新的數組內存地址返回給數組變量;移除操做也相似,只是方式是移除元素而不是添加元素;而查詢方法,由於不涉及線程操做,因此並無加鎖出來!
由於CopyOnWriteArrayList
讀取內容沒有加鎖,在寫入數據的時候同時也能夠進行讀取數據操做,所以性能獲得很大的提高,可是也有缺陷,對於邊讀邊寫的狀況,不必定能實時的讀到最新的數據,好比以下操做:
public static void main(String[] args) throws InterruptedException {
final CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();
list.add("a");
list.add("b");
for (int i = 0; i < 5; i++) {
final int j =i;
new Thread(new Runnable() {
@Override
public void run() {
//寫入數據
list.add("i-" + j);
//讀取數據
for (String str : list) {
System.out.println("線程-" + Thread.currentThread().getName() + ",讀取內容:" + str);
}
}
}).start();
}
}
新建5個線程向list
中添加元素,執行結果以下:
能夠看到,5個線程的讀取內容有差別!
所以CopyOnWriteArrayList
很適合讀多寫少的應用場景!
一、JDK1.7&JDK1.8 源碼
二、掘金 - 擁抱心中的夢想 - 說一說Java中的CopyOnWriteArrayList