大家好,我是三友,這篇文章想來跟大家來探討一下,在Java中已經提供了併發安全的集合,為什麼有的場景還需要使用讀寫鎖,直接用併發安全的集合難道不行麽? 在java中,併發安全的集合有很多,這裡我就選用常見的CopyOnWriteArrayList為例,來說明一下讀寫鎖的價值到底提現在哪。 CopyO ...
大家好,我是三友,這篇文章想來跟大家來探討一下,在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 = 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 = 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日記 ,回覆 併發 就能獲取了。