大家好,我是三友,這篇文章想來跟大家來探討一下,在Java中已經提供了并發安全的集合,為什么有的場景還需要使用讀寫鎖,直接用并發安全的集合難道不行么?
在java中,并發安全的集合有很多,這里我就選用常見的CopyOnWriteArrayList為例,來說明一下讀寫鎖的價值到底提現在哪,
CopyOnWriteArrayList核心原始碼分析
接下來我們分析一下CopyOnWriteArrayList核心的增刪改查的方法
成員變數
//獨占鎖 final transient ReentrantLock lock = new ReentrantLock(); //底層用來存放元素的陣列 private transient volatile Object[] array;
add方法:往集合中添加某個元素
public boolean add(E e) {
final ReentrantLock lock = this.lock;
lock.lock();
try {
Object[] elements = getArray();
int len = elements.length;
Object[] newElements = Arrays.copyOf(elements, len + 1);
newElements[len] = e;
setArray(newElements);
return true;
} finally {
lock.unlock();
}
}
add操作先通過lock加鎖,保證同一時刻最多只有一個執行緒可以操作,加鎖成功獲取到成員變數的資料,然后拷貝成員變數陣列的元素到新的陣列,再基于新的資料來添加元素,最后將新拷貝的陣列通過setArray來替換舊的成員變數的陣列,
remove方法:移除集合中的某個元素
public E remove(int index) {
final ReentrantLock lock = this.lock;
lock.lock();
try {
Object[] elements = getArray();
int len = elements.length;
E oldValue = https://www.cnblogs.com/zzyang/p/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操作也要先獲取到鎖,它先是取出對應陣列下標的舊元素,然后新建了一個原陣列長度減1的新陣列,將除了被移除的元素之外,剩余的元素拷貝到新的陣列,最后再通過setArray替換舊的成員變數的陣列,
set方法:將集合中指定位置的元素替換成新的元素
public E set(int index, E element) {
final ReentrantLock lock = this.lock;
lock.lock();
try {
Object[] elements = getArray();
E oldValue = https://www.cnblogs.com/zzyang/p/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
setArray(elements);
}
return oldValue;
} finally {
lock.unlock();
}
}
set方法跟add,remove操作一樣得先獲取到鎖才能繼續執行,將原陣列的原有元素拷貝到新的陣列上,在新的陣列完成資料的替換,最后也是通過setArray替換舊的成員變數的陣列,
size方法:獲取集合中元素的個數
public int size() {
return getArray().length;
}
size方法操作很簡單,就是簡單地回傳一下當前陣列的長度,
迭代器的構造
public Iterator<E> iterator() {
return new COWIterator<E>(getArray(), 0);
}
構造COWIterator的時候傳入當前陣列的物件,然后基于當前陣列來遍歷,也不需要加鎖,
講完CopyOnWriteArrayList原始碼,我們可以看出CopyOnWriteArrayList的核心原理就是在對陣列進行增刪改的時候全部都是先加獨占鎖,然后對原有的陣列進行拷貝,然后基于新復制的陣列進行操作,最后將這個新的陣列替換成員變數的陣列;而對于讀的操作來說,都是不加鎖的,是基于當前成員變數的陣列的這一時刻的快照來讀的,其實CopyOnWriteArrayList是基于一種寫時復制的思想,寫的時候基于新拷貝的陣列來操作,之后再賦值給成員變數,讀的時候是原有的陣列,這樣讀寫其實就是不是同一個陣列,這樣就避免了讀寫沖突的情況,這其實也體現了一種讀寫分離的思想,讀寫操作的是不同的陣列,
CopyOnWriteArrayList適用場景
接下來我們來思考一下,CopyOnWriteArrayList適合使用在什么樣的場景中,通過上面原始碼的分析,我們可以看出,所有的寫操作,包括增刪改都需要加同一把獨占鎖,所以同時只允許一個執行緒對陣列進行拷貝賦值的操作,多執行緒并發情況下所有的操作都是串行執行的,勢必會導致并發能力降低,同時每次操作都涉及到了陣列的拷貝,性能也不太好;而所有的讀操作都不需要加鎖,所以同一時間可以允許大量的執行緒同時讀,并發性能高,所以綜上我們可以得出一個結論,那就是CopyOnWriteArrayList適合讀多寫少的場景,
CopyOnWriteArrayList的局限性
說完CopyOnWriteArrayList,我們來想一想它有沒有什么缺點,看起來CopyOnWriteArrayList除了寫的并發性能差點,好像沒有什么缺點了,的確,單從性能來看,確實是這種情況,但是,從資料一致性的角度來看,CopyOnWriteArrayList的資料一致性能力較弱,屬于資料弱一致性,所謂的弱一致性,你可以這么理解,在某一個時刻,讀到的資料并不是當前這一時刻最新的資料,
就拿CopyOnWriteArrayList舉例來說,當有個執行緒A正在呼叫add方法來添加元素,此時已經完成了陣列的拷貝,并且也將元素添加到陣列中,但是還沒有將新的陣列賦值給成員變數,此時,另一個執行緒B來呼叫CopyOnWriteArrayList的size方法,來讀取集合中元素的個數,那么此時讀到的元素個數其實是不包括執行緒A要添加的元素,因為執行緒A并沒有將新的陣列賦值給成員變數,這就導致了執行緒B讀到的資料不是最新的資料,也就是跟實際的資料不一致,
所以,從上面我們可以看出,CopyOnWriteArrayList對于資料一致性的保證,還是比較弱的,其實不光是CopyOnWriteArrayList,其實Java中的很多集合,佇列的實作對于資料一致性的保證都比較弱,
如何來保證資料的強一致性
那么有什么好的辦法可以保證資料的強一致性么?當然,保證并發安全,加鎖就可以完成,但是加什么鎖可以保證資料讀寫安全和資料一致性,其實最簡單粗暴的方法就是對所有的讀寫都加上同一把獨占鎖,這樣保證所有的讀寫操作都是串行執行,那么讀的時候,其他執行緒一定不能寫,那么讀的一定是最新的資料,
如果真的這么去加獨占鎖,的確能夠保證讀寫安全,但是性能卻會很差,這也是為什么CopyOnWriteArrayList的讀不加鎖的原因,其實CopyOnWriteArrayList在設計的時候,就是降低資料一致性來換取讀的性能,
那有沒有什么折中的方法,既能保證讀的性能不差,又能保證資料強一致性呢,這時就可以用讀寫鎖來實作,所謂的讀寫鎖,就是寫的時候,其他執行緒不能寫也不能讀,讀的時候,其他執行緒能讀,但是不能寫,也就是寫寫、讀寫互斥,但是讀讀不互斥,基于這種方式,就能保證讀的時候,一定沒有人在寫,這樣讀到的資料就一定是最新的,同時也能保證其他執行緒也能讀,不會出現上面舉例的那種情況了,也就能保證資料的強一致性,讀寫鎖相比獨占鎖而言,大大提高了讀的并發能力,但是寫的時候不能讀,相比于CopyOnWriteArrayList而言,讀的并發能力有所降低,這可能就是魚(并發性能)和熊掌(資料一致性)不可兼得吧,
Java中也提供了讀寫鎖的實作,ReentrantReadWriteLock,底層是基于AQS來實作的,有興趣的小伙伴可以翻一下原始碼,看看是如何實作的,這里就不再剖析原始碼了,
總結
好了,通過這篇文章,想必大家知道為什么有并發安全的集合之后,還需要讀寫鎖的原因,因為很多并發安全的集合對于資料一致性的保證是比較弱的,一旦遇到對于資料一致性要求比較高的場景,一些并發安全的集合就不適用了;同時為了避免獨占鎖帶來的性能問題,可以選擇讀寫鎖來保證讀的并發能力,小伙伴們在實際應用中需要根據應用場景來靈活地選擇使用并發安全的集合、讀寫鎖或者是獨占鎖,其實永遠沒有最好的選擇,只有更好的選擇,
以上就是本篇文章的全部內容,如果你有什么不懂或者想要交流的地方,歡迎關注我的個人的微信公眾號 三友的java日記 ,我們下篇文章再見,
如果覺得這篇文章對你有所幫助,還請幫忙點贊、在看、轉發一下,碼字不易,非常感謝!
最近花了一個月的時間,整理了這套并發編程系列的知識點,涵蓋了 volitile、synchronized、CAS、AQS、鎖優化策略、同步組件、資料結構、執行緒池、Thread、ThreadLocal,幾乎覆寫了所有的學習和面試場景,如圖,





檔案獲取方式:掃描二維碼或者搜一搜關注微信公眾號 三友的java日記 ,回復 并發 就能獲取了,

轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/482126.html
標籤:Java
