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是典型的以空间换时间的模型,实现的容器优点和缺点都突出,优点是性能非常高,因为没有锁。但存在两个问题,第一是内存占用会很高,因为在复制出新数组后,旧的数组中的对象并不会马上销毁,如果数组很大,且并发更新很高时,搞不好会出现内存溢出。可以通过一些方法压缩内容,避免这种问题;第二是存在内存一致性问题,虽然最终内存还是一致的,但如果在复制新数组过程中,引用还未指向新数组时,线程访问会出现读不到新的元素。