概述
对于JAVA并发并发编程我们常需要了解并发底层原理、同步组件、同步集合、线程池还有就是并发容器了,对于并发容器今天讲解下CopyOnWriteArrayList这个替代List的并发集合
CopyOnWriteArrayList类重要成员结构图如下:
其中lock成员变量便是为什么CopyOnWriteArrayList这个集合在多线程下任然能正确的运行的关键;array这个成员变量就是CopyOnWriteArrayList底层用于封装数据的结构,可以理解为CopyOnWriteArrayList就是一个被改造过了的数组。知道这个两个成员变量可以有效的理解CopyOnWriteArrayList的底层原理。
方法解析
1)add(E e)添加元素到末尾
public boolean add(E e) {
final ReentrantLock lock = this.lock;//添加元素前获取重入锁
lock.lock();//加锁
try {
Object[] elements = getArray();//获取以前的数组给elements
int len = elements.length;//获取原数组的长度
//将原数组内容复制到一个长度比以前多一个长度的新数组
//这里是关键,解释了CopyOnWriteArrayList的本质“写时复制”
//即添加元素时会复制出一个新数组,在新数组上添加元素,而不是直接改动原数组
Object[] newElements = Arrays.copyOf(elements, len + 1);
//这里就解释了,添加元素的时候是在新数组上改动,不改动原数组
newElements[len] = e;
//这里就是把新数组的引用传递给CopyOnWriteArrayList的array这个成员,
//即现在新数组成为了CopyOnWriteArrayList的底层容器,老数组等待被GC
setArray(newElements);
return true;
} finally {
lock.unlock();//添加元素后释放锁
}
}
总结:可以看出CopyOnWriteArrayList底层用数组装元素,每次添加的时候是直接复制出一个新数组,长度只比以前多一个元素的长度,而且他是通过加锁的方式保证了添加元素在多线程下的正确性;所以也可以看出CopyOnWriteArrayList的改动元素的效率低。
2)add(E e,int index)添加元素到指定位置
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)//如果移动步数为0则是添加到末尾
newElements = Arrays.copyOf(elements, len + 1);//那么这里就按add(E e)的方式处理了
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);//把新数组引用赋予array
} finally {
lock.unlock();
}
}
总结:添加元素到指定位置,总的实现思路和添加元素到末尾差不多,唯一的不同就每次复制元素的适合要分两次复制,一次复制前面的,一次复制后面的。
3)remove(int index)删除指定位置元素
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)//如果为0就代表是删除最后元素
setArray(Arrays.copyOf(elements, len - 1));//那就把原数组前n-1个元素复制到新数组中并把引用赋予array
else {
Object[] newElements = new Object[len - 1];//创建一个长度减1的新数组
System.arraycopy(elements, 0, newElements, 0, index);//复制原数组前index个元素到新数组同样位置
System.arraycopy(elements, index + 1, newElements, index,//复制原数组后index+个元素到新数组同样位置
numMoved);
setArray(newElements);//改变array的引用赋予CopyOnWriteArrayList的成员变量,老数组等待GC
}
return oldValue;
} finally {
lock.unlock();
}
}
总结:删除元素,大体实现思路和添加等方法的实现差不多,不同的就是分两次复制原数组删除位置的前面的元素到新数组,复制删除元素位置后面的元素到新数组。
总结:从CopyOnWriteArrayList的源码可以看出,添加、删除等修改元素的操作都是通过新建数组在新数组上操作 ,最后把新数组的赋予CopyOnWriteArrayList的array。其中,修改操作都加了锁,所以修改操作是安全的,但效率不高
4)get(int index)获取指定位置的元素
public E get(int index) {
return get(getArray(), index);//获取元素是从原数组中指定位置获取,
//这也是为什么CopyOnWriteArrayList并发时//读的效率很高且没有安全,
//因为没加锁,所以高,因为只是读操作所以是安全的
}
CopyOnWriteArrayList的弱应用迭代器
特点:都知道一般的集合在迭代的过程中,如果同时在改变集合中的元素,那么会报并发异常,而这底层的原理是JDK的设计者在设计集合的适合,考虑到了这一点,但却不想用加锁的方式来保证其安全,所以采用了fail-fast的机制。具体的原理待下一篇博客
而CopyOnWriteArrayList在迭代的时候如果有元素在改变集合的元素,那么不会报并发错误,同时修改的元素也不会再迭代中体现。具体的原理看下面的源码分析
//COWIterator这个就是我们常说的Iterator
static final class COWIterator<E> implements ListIterator<E> {
/** Snapshot of the array */
private final Object[] snapshot;//这个是原数组的快照
/** Index of element to be returned by subsequent call to next. */
private int cursor;//数组中元素下标
private COWIterator(Object[] elements, int initialCursor) {
cursor = initialCursor;
snapshot = elements;
}
public boolean hasNext() {
return cursor < snapshot.length;
}
public boolean hasPrevious() {
return cursor > 0;
}
@SuppressWarnings("unchecked")
public E next() {
if (! hasNext())
throw new NoSuchElementException();
return (E) snapshot[cursor++];
}
@SuppressWarnings("unchecked")
public E previous() {
if (! hasPrevious())
throw new NoSuchElementException();
return (E) snapshot[--cursor];
}
public int nextIndex() {
return cursor;
}
public int previousIndex() {
return cursor-1;
}
/**
* Not supported. Always throws UnsupportedOperationException.
* @throws UnsupportedOperationException always; {@code remove}
* is not supported by this iterator.
*/
public void remove() {
throw new UnsupportedOperationException();
}
总结:可以从源码中看出,之所以不会再迭代的时候改变不会报错也不会在迭代过程中体现,是因为CopyOnWriteArrayList在迭代的时候会获取原数组的快照(可以理解为一个复制版本),之后的迭代都是对这个复制版进行迭代的,改变的时候是改变的不同这个快照的数组,相互不影响。
总结
1)CopyOnWriteArrayList适合读操作多余写操作的情况,读操作效率高,写操作效率低
2)CopyOnWriteArrayList的迭代器是若引用,在迭代中改变集合元素时不报错