寫入時複製(CopyOnWrite)與 讀寫鎖
一、CopyOnWrite 思想
寫入時複製(CopyOnWrite,簡稱COW)思想是計算機程式設計領域中的一種通用優化策略。其核心思想是,如果有多個呼叫者(Callers)同時訪問相同的資源(如記憶體或者是磁碟上的資料儲存),他們會共同獲取相同的指標指向相同的資源,直到某個呼叫者修改資源內容時,系統才會真正複製一份專用副本(private copy)給該呼叫者,而其他呼叫者所見到的最初的資源仍然保持不變。這過程對其他的呼叫者都是透明的(transparently)。此做法主要的優點是如果呼叫者沒有修改資源,就不會有副本(private copy)被建立,因此多個呼叫者只是讀取操作時可以共享同一份資源。
通俗易懂的講,寫入時複製技術就是不同程序在訪問同一資源的時候,只有更新操作,才會去複製一份新的資料並更新替換,否則都是訪問同一個資源。
JDK 的 CopyOnWriteArrayList/CopyOnWriteArraySet 容器正是採用了 COW 思想,它是如何工作的呢?簡單來說,就是平時查詢的時候,都不需要加鎖,隨便訪問,只有在更新的時候,才會從原來的資料複製一個副本出來,然後修改這個副本,最後把原資料替換成當前的副本。修改操作的同時,讀操作不會被阻塞,而是繼續讀取舊的資料。這點要跟讀寫鎖區分一下。
二、原始碼分析
我們先來看看 CopyOnWriteArrayList 的 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(); } }
private E get(Object[] a, int index) {
return (E) a[index];
}
/**
* {@inheritDoc}
*
* @throws IndexOutOfBoundsException {@inheritDoc}
*/
public E get(int index) {
return get(getArray(), index);
}
三、優點和缺點
1.優點
對於一些讀多寫少的資料,寫入時複製的做法就很不錯,例如配置、黑名單、物流地址等變化非常少的資料,這是一種無鎖的實現。可以幫我們實現程式更高的併發。
CopyOnWriteArrayList 併發安全且效能比 Vector 好。Vector 是增刪改查方法都加了synchronized 來保證同步,但是每個方法執行的時候都要去獲得鎖,效能就會大大下降,而 CopyOnWriteArrayList 只是在增刪改上加鎖,但是讀不加鎖,在讀方面的效能就好於 Vector。
2.缺點
資料一致性問題。這種實現只是保證資料的最終一致性,不能保證資料的實時一致性;在新增到拷貝資料而還沒進行替換的時候,讀到的仍然是舊資料。
記憶體佔用問題。在進行寫操作的時候,記憶體裡會同時駐紮兩個物件的記憶體,如果物件比較大,頻繁地進行替換會消耗記憶體,從而引發 Java 的 GC 問題,這個時候,我們應該考慮其他的容器,例如 ConcurrentHashMap。
四、和讀寫鎖比較
讀寫鎖:分為讀鎖和寫鎖,多個讀鎖不互斥,讀鎖與寫鎖互斥。
總之,讀的時候上讀鎖,寫的時候上寫鎖! Java裡面的實現:ReentrantReadWriteLock
ReadWriteLock mylock = new ReentrantReadWriteLock(false);
- 讀鎖lock、unlock:
myLock.readLock().lock();
myLock.readLock().unlock();
- 寫鎖lock、unlock:
myLock.writeLock().lock();
myLock.writeLock().unlock();
總結:讀寫鎖是遵循寫寫互斥、讀寫互斥、讀讀不互斥的原則,而copyOnWrite則是寫寫互斥、讀寫不互斥、讀讀不互斥的原則。