Java併發包提供了很多線程安全的集合,有了他們的存在,使得我們在多線程開發下,可以和單線程一樣去編寫代碼,大大簡化了多線程開發的難度,但是如果不知道其中的原理,可能會引發意想不到的問題,所以知道其中的原理還是很有必要的。 今天我們來看下Java併發包中提供的線程安全的List,即CopyOnWri ...
Java併發包提供了很多線程安全的集合,有了他們的存在,使得我們在多線程開發下,可以和單線程一樣去編寫代碼,大大簡化了多線程開發的難度,但是如果不知道其中的原理,可能會引發意想不到的問題,所以知道其中的原理還是很有必要的。
今天我們來看下Java併發包中提供的線程安全的List,即CopyOnWriteArrayList。
剛接觸CopyOnWriteArrayList的時候,我總感覺這個集合的名稱有點奇怪:在寫的時候複製?後來才知道它就是在寫的時候進行了複製,所以這個命名還是相當嚴謹的。當然,翻譯成 寫時複製 會更好一些。
我們在研究源碼的時候,可以帶著問題去研究,這樣可能效果會更好,把問題一個一個攻破,也更有成就感,所以在這裡,我先拋出幾個問題:
- CopyOnWriteArrayList如何保證線程安全性的。
- CopyOnWriteArrayList長度有沒有限制。
- 為什麼說CopyOnWriteArrayList是一個寫時複製集合。
我們先來看下CopyOnWriteArrayList的UML圖:
主要方法源碼解析
add
我們可以通過add方法添加一個元素
public boolean add(E e) {
//1.獲得獨占鎖
final ReentrantLock lock = this.lock;
lock.lock();
try {
Object[] elements = getArray();//2.獲得Object[]
int len = elements.length;//3.獲得elements的長度
Object[] newElements = Arrays.copyOf(elements, len + 1);//4.複製到新的數組
newElements[len] = e;//5.將add的元素添加到新元素
setArray(newElements);//6.替換之前的數據
return true;
} finally {
lock.unlock();//7.釋放獨占鎖
}
}
final Object[] getArray() {
return array;
}
當調用add方法,代碼會跑到(1)去獲得獨占鎖,因為獨占鎖的特性,導致如果有多個線程同時跑到(1),只能有一個線程成功獲得獨占鎖,並且執行下麵的代碼,其餘的線程只能在外面等著,直到獨占鎖被釋放。
線程獲得到獨占鎖後,執行(2),獲得array,並且賦值給elements ,(3)獲得elements的長度,並且賦值給len,(4)複製elements數組,在此基礎上長度+1,賦值給newElements,(5)將我們需要新增的元素添加到newElements,(6)替換之前的數組,最後跑到(7)釋放獨占鎖。
解析源碼後,我們明白了
- CopyOnWriteArrayList是如何保證【寫】時線程安全的?因為用了ReentrantLock獨占鎖,保證同時只有一個線程對集合進行修改操作。
- 數據是存儲在CopyOnWriteArrayList中的array數組中的。
- 在添加元素的時候,並不是直接往array裡面add元素,而是複製出來了一個新的數組,並且複製出來的數組的長度是 【舊數組的長度+1】,再把舊的數組替換成新的數組,這是尤其需要註意的。
get
public E get(int index) {
return get(getArray(), index);
}
final Object[] getArray() {
return array;
}
我們可以通過調用get方法,來獲得指定下標的元素。
首先獲得array,然後獲得指定下標的元素,看起來沒有任何問題,但是其實這是存在問題的。別忘了,我們現在是多線程的開發環境,不然也沒有必要去使用JUC下麵的東西了。
試想這樣的場景,當我們獲得了array後,把array捧在手心裡,如獲珍寶。。。由於整個get方法沒有獨占鎖,所以另外一個線程還可以繼續執行修改的操作,比如執行了remove的操作,remove和add一樣,也會申請獨占鎖,並且複製出新的數組,刪除元素後,替換掉舊的數組。而這一切get方法是不知道的,它不知道array數組已經發生了天翻地覆的變化,它還是傻乎乎的,看著捧在手心裡的array。。。這就是弱一致性。
就像微信一樣,雖然對方已經把你給刪了,但是你不知道,你還是每天打開和她的聊天框,準備說些什麼。。。
set
我們可以通過set方法修改指定下標元素的值。
public E set(int index, E element) {
//(1)獲得獨占鎖
final ReentrantLock lock = this.lock;
lock.lock();
try {
Object[] elements = getArray();//(2)獲得array
E oldValue = get(elements, index);//(3)根據下標,獲得舊的元素
if (oldValue != element) {//(4)如果舊的元素不等於新的元素
int len = elements.length;//(5)獲得舊數組的長度
Object[] newElements = Arrays.copyOf(elements, len);//(6)複製出新的數組
newElements[index] = element;//(7)修改
setArray(newElements);//(8)替換
} else {
//(9)為了保證volatile 語義,即使沒有修改,也要替換成新的數組
// Not quite a no-op; ensures volatile write semantics
setArray(elements);
}
return oldValue;
} finally {
lock.unlock();//(10)釋放獨占鎖
}
}
當我們調用set方法後:
- 和add方法一樣,先獲取獨占鎖,同樣的,只有一個線程可以獲得獨占鎖,其他線程會被阻塞。
- 獲取到獨占鎖的線程獲得array,並且賦值給elements。
- 根據下標,獲得舊的元素。
- 進行一個對比,檢查舊的元素是否不等於新的元素,如果成立的話,執行5-8,如果不成立的話,執行9。
- 獲得舊數組的長度。
- 複製出新的數組。
- 修改新的數組中指定下標的元素。
- 把舊的數組替換掉。
- 為了保證volatile語義,即使沒有修改,也要替換成新的數組。
- 不管是否執行了修改的操作,都會釋放獨占鎖。
通過源碼解析,我們應該更有體會:
- 通過獨占鎖,來保證【寫】的線程安全。
- 修改操作,實際上操作的是array的一個副本,最後才把array給替換掉。
remove
我們可以通過remove刪除指定坐標的元素。
public E remove(int index) {
final ReentrantLock lock = this.lock;
lock.lock();
try {
Object[] elements = getArray();
int len = elements.length;
E oldValue = get(elements, index);
int numMoved = len - index - 1;
if (numMoved == 0)
setArray(Arrays.copyOf(elements, len - 1));
else {
Object[] newElements = new Object[len - 1];
System.arraycopy(elements, 0, newElements, 0, index);
System.arraycopy(elements, index + 1, newElements, index,
numMoved);
setArray(newElements);
}
return oldValue;
} finally {
lock.unlock();
}
}
可以看到,remove方法和add,set方法是一樣的,第一步還是先獲取獨占鎖,來保證線程安全性,如果要刪除的元素是最後一個,則複製出一個長度為【舊數組的長度-1】的新數組,隨之替換,這樣就巧妙的把最後一個元素給刪除了,如果要刪除的元素不是最後一個,則分兩次複製,隨之替換。
迭代器
在解析源碼前,我們先看下迭代器的基本使用:
public class Main {public static void main(String[] args) {
CopyOnWriteArrayList<String> copyOnWriteArrayList=new CopyOnWriteArrayList<>();
copyOnWriteArrayList.add("Hello");
copyOnWriteArrayList.add("copyOnWriteArrayList");
Iterator<String>iterator=copyOnWriteArrayList.iterator();
while (iterator.hasNext()){
System.out.println(iterator.next());
}
}
}
運行結果:
代碼很簡單,這裡就不再解釋了,我們直接來看迭代器的源碼:
public Iterator<E> iterator() {
return new COWIterator<E>(getArray(), 0);
}
static final class COWIterator<E> implements ListIterator<E> {
private final Object[] snapshot;
private int cursor;
private COWIterator(Object[] elements, int initialCursor) {
cursor = initialCursor;
snapshot = elements;
}
// 判斷是否還有下一個元素
public boolean hasNext() {
return cursor < snapshot.length;
}
//獲取下個元素
@SuppressWarnings("unchecked")
public E next() {
if (! hasNext())
throw new NoSuchElementException();
return (E) snapshot[cursor++];
}
當我們調用iterator方法獲取迭代器,內部會調用COWIterator的構造方法,此構造方法有兩個參數,第一個參數就是array數組,第二個參數是下標,就是0。隨後構造方法中會把array數組賦值給snapshot變數。
snapshot是“快照”的意思,如果Java基礎尚可的話,應該知道數組是引用類型,傳遞的是指針,如果有其他地方修改了數組,這裡應該馬上就可以反應出來,那為什麼又會是snapshot這樣的命名呢?沒錯,如果其他線程沒有對CopyOnWriteArrayList進行增刪改的操作,那麼snapshot就是本身的array,但是如果其他線程對CopyOnWriteArrayList進行了增刪改的操作,舊的數組會被新的數組給替換掉,但是snapshot還是原來舊的數組的引用。也就是說 當我們使用迭代器便利CopyOnWriteArrayList的時候,不能保證拿到的數據是最新的,這也是弱一致性問題。
什麼?你不信?那我們通過一個demo來證實下:
public static void main(String[] args) throws InterruptedException {
CopyOnWriteArrayList<String> copyOnWriteArrayList=new CopyOnWriteArrayList<>();
copyOnWriteArrayList.add("Hello");
copyOnWriteArrayList.add("CopyOnWriteArrayList");
copyOnWriteArrayList.add("2019");
copyOnWriteArrayList.add("good good study");
copyOnWriteArrayList.add("day day up");
new Thread(()->{
copyOnWriteArrayList.remove(1);
copyOnWriteArrayList.remove(3);
}).start();
TimeUnit.SECONDS.sleep(3);
Iterator<String> iterator = copyOnWriteArrayList.iterator();
while (iterator.hasNext()) {
System.out.println(iterator.next());
}
}
運行結果:
這沒問題把,我們先是往list裡面add了點數據,然後開一個線程,線上程裡面刪除一些元素,睡3秒是為了保證線程運行完畢。然後獲取迭代器,遍歷元素,發現被remove的元素沒有被列印出來。
然後我們換一種寫法:
public static void main(String[] args) throws InterruptedException {
CopyOnWriteArrayList<String> copyOnWriteArrayList=new CopyOnWriteArrayList<>();
copyOnWriteArrayList.add("Hello");
copyOnWriteArrayList.add("CopyOnWriteArrayList");
copyOnWriteArrayList.add("2019");
copyOnWriteArrayList.add("good good study");
copyOnWriteArrayList.add("day day up");
Iterator<String> iterator = copyOnWriteArrayList.iterator();
new Thread(()->{
copyOnWriteArrayList.remove(1);
copyOnWriteArrayList.remove(3);
}).start();
while (iterator.hasNext()) {
System.out.println(iterator.next());
}
}
這次我們改變了代碼的順序,先是獲取迭代器,然後是執行刪除線程的操作,最後遍歷迭代器。
運行結果:
可以看到被刪除的元素,還是列印出來了。
如果我們沒有分析源碼,不知道其中的原理,不知道弱一致性,當在多線程中用到CopyOnWriteArrayList的時候,可能會痛不欲生,想砸電腦,不知道為什麼獲取的數據有時候就不是正確的數據,而有時候又是。所以探究原理,還是挺有必要的,不管是通過源碼分析,還是通過看博客,甚至是直接看JDK中的註釋,都是可以的。
在Java併發包提供的集合中,CopyOnWriteArrayList應該是最簡單的一個,希望通過源碼分析,讓大家有一個信心,原來JDK源碼也是可以讀懂的。