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使用,相關代碼如下:
| 12
 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數組引用的下標找到對應的元素返回即可:
| 12
 3
 | public E get(int index) {return (E)(getArray()[index]);
 }
 | 
 
 
其它的方法實現也很簡單,涉及到更新數組的操作都會加鎖和復制新數組。
三、總結
CopyOnWrite是典型的以空間換時間的模型,實現的容器優點和缺點都突出,優點是性能非常高,因為沒有鎖。但存在兩個問題,第一是內存占用會很高,因為在復制出新數組后,舊的數組中的對象并不會馬上銷毀,如果數組很大,且并發更新很高時,搞不好會出現內存溢出。可以通過一些方法壓縮內容,避免這種問題;第二是存在內存一致性問題,雖然最終內存還是一致的,但如果在復制新數組過程中,引用還未指向新數組時,線程訪問會出現讀不到新的元素。