CopyOnWrite是一種優化策略,當在并發寫時,通過復制出原內容,在新的內容上讀寫,然后再把新的內容指向原來內容的引用。通過這種策略實現并發時,讀寫互不干涉,類似讀寫分離,來提升性能。
CopyOnWrite容器是寫時復制容器,實現讀無鎖,提高讀的并發性能。 Java目前提供了兩個CopyOnWrite容器,分別為CopyOnWriteArrayList和CopyOnWriteArraySet,CopyOnWriteArraySet的實現完全是通過CopyOnWriteArrayList來實現的,它持有一個final的CopyOnWriteArrayList引用,把方法調用都委托給CopyOnWriteArrayList來調用。
下面只研究CopyOnWriteArrayList的實現:
一、CopyOnWriteArrayList的結構
CopyOnWriteArrayList的結構比較簡單,它只包含了兩個成員變量:volatile的array對象數組和ReentrantLock對象。volatile的array引用可以保證修改時對其它線程可見,ReentrantLock鎖主要運用在對數組的寫操作上。
二、主要方法實現分析
1、add的相關方法實現
add有3個方法,一個是直接添加到數組中,一個是添加到數組中的指定位置,還有一個是如果數組中沒有元素時才添加成功的方法,主要是給CopyOnWriteArraySet使用,相關代碼如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70
|
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(); } } public void add(int index, E element) { final ReentrantLock lock = this.lock; lock.lock(); try { Object[] elements = getArray(); int len = elements.length; if (index > len || index < 0) throw new IndexOutOfBoundsException("Index: "+index+ ", Size: "+len); Object[] newElements; int numMoved = len - index; if (numMoved == 0) newElements = Arrays.copyOf(elements, len + 1); else { newElements = new Object[len + 1]; System.arraycopy(elements, 0, newElements, 0, index); System.arraycopy(elements, index, newElements, index + 1, numMoved); } newElements[index] = element; setArray(newElements); } finally { lock.unlock(); } } public boolean addIfAbsent(E e) { final ReentrantLock lock = this.lock; lock.lock(); try { Object[] elements = getArray(); int len = elements.length; Object[] newElements = new Object[len + 1]; for (int i = 0; i < len; ++i) { if (eq(e, elements[i])) return false; else newElements[i] = elements[i]; } newElements[len] = e; setArray(newElements); return true; } finally { lock.unlock(); } }
|
從上面的代碼可以看出,3者的實現大同小異,主要流程如下:
1).首先加鎖,然后獲取舊數組,復制出一份新數組,然后在新數組上添加元素,最后把對象數組的引用指用新數組;
2).不同的是add(E e)直接把添加的元素添加到新數組的最后位置,而add(int index ,E e)方法在復制舊數組時不同,需要計算出index前后的長度,再復制;addIfAbsent(E e)則是會判斷數組中是否已經存在要添加的元素,如果存在返回false。
2、get方法實現
get方法實現很簡單,直接從array數組引用的下標找到對應的元素返回即可:
1 2 3
|
public E get(int index) { return (E)(getArray()[index]); }
|
其它的方法實現也很簡單,涉及到更新數組的操作都會加鎖和復制新數組。
三、總結
CopyOnWrite是典型的以空間換時間的模型,實現的容器優點和缺點都突出,優點是性能非常高,因為沒有鎖。但存在兩個問題,第一是內存占用會很高,因為在復制出新數組后,舊的數組中的對象并不會馬上銷毀,如果數組很大,且并發更新很高時,搞不好會出現內存溢出。可以通過一些方法壓縮內容,避免這種問題;第二是存在內存一致性問題,雖然最終內存還是一致的,但如果在復制新數組過程中,引用還未指向新數組時,線程訪問會出現讀不到新的元素。