CopyOnWriteArrayList解析
1. Vector和SynchronizedList
1.1 回顾
? 众所周知,ArrayList是用于替代Vector的,Vector是线程安全的容器,因为它几乎在每个方法声明处都加了synchronized关键字来使容器安全。
? 如果使用Collections.synchronizedList(new ArrayList())
来使得ArrayList线程安全的话,也是几乎在每个方法上都加上了synchronized关键字,不同的是,它是加在方法内部,而不是方法声明上。
1.2 可能存在的问题
? 不管是使用Vector还是使用SynchronizedList,在多个线程中对该对象进行操作时,都会发生线程安全问题。唯一的解决办法就是在线程中操作对象之前,给该集合对象加锁,但是这样每次进行操作时都需要加锁,程序的效率会大打折扣。
? 因此,就需要使用CopyOnWriteArrayList
2. CopyOnWriteArrayListSet介绍
? 一般来说,我们都认为:CopyOnWriteArrayList是同步List的替代品,CopyOnWriteArrayListSet是同步Set的替代品。
? 无论是HashTable==》ConcurrentHashMap,还是Vector==》CopyOnWriteArrayList。JUC下支持并发的容器与老一代的线程安全类相比,总结起来就是加锁粒度的区别。
- HashTable、Vector加锁的粒度大,直接在方法声明处使用synchronized。
- ConcurrentHashMap、CopyOnWriteArrayList加锁粒度小 (用各种方式来实现线程安全,比如ConcurrentHashMap使用了cas锁、volatile等方法实现线程安全)
- JUC下的线程安全容器在遍历的时候不会抛出ConcurrentModificationException异常
? 所以一般来说,我们都会使用JUC包下提供的线程安全容器,而不是老一代的线程安全容器。
3. CopyOnWriteArrayList实现原理
如果有多个调用者 (callers) 同时请求相同资源 (如内存或磁盘上的数据存储) ,他们会共同获取相同的指针指向相同的资源,直到某个调用者试图修改资源的内容时,系统才会真正复制一份专用的副本给该调用者,而其他调用者所见到的最初的资源仍然保持不变。
优点是如果调用者没有修改该资源,就不会有副本被建立,因此多个调用者只是读取操作时可以共享同一份资源。
- CopyOnWriteArrayList是线程安全容器 (相对于ArrayList) ,底层通过复制数组的方法来实现。
- CopyOnWriteArrayList在遍历的使用不会抛出ConcurrentModificationException异常,并且遍历的时候就不用额外加锁
- 元素可以为null
4. CopyOnWriteArrayList源码
4.1 属性和构造方法
public class CopyOnWriteArrayList<E>implements List<E>, RandomAccess, Cloneable, java.io.Serializable {//重入锁final transient ReentrantLock lock = new ReentrantLock();//存储数组,只能通过getArray/setArray访问private transient volatile Object[] array;//获取数组final Object[] getArray() {return array;}//设置数组final void setArray(Object[] a) {array = a;}//初始化数组 设置一个空数组public CopyOnWriteArrayList() {setArray(new Object[0]);}
}
4.2 常见方法
根据线程安全分析,如果遍历Vector/SynchronizedList是需要自己手动加锁的。
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();} }
-
size方法
public int size() {return getArray().length; }
-
get方法
public E get(int index) {return get(getArray(), index); }private E get(Object[] a, int index) {return (E) a[index]; }
-
set方法
public E set(int index, E element) {//加锁final ReentrantLock lock = this.lock;lock.lock();try {Object[] elements = getArray();E oldValue = get(elements, index);//原元素与新元素不同,则覆盖原来的元素if (oldValue != element) {int len = elements.length;//操作的是副本数组,此时get方法不受影响Object[] newElements = Arrays.copyOf(elements, len);newElements[index] = element;setArray(newElements);} else {//元素相同,无操作setArray(elements);}return oldValue;} finally {//完成操作后解锁lock.unlock();} }
-
对于remove,clear方法跟set,add类似
总结:
- 在修改时,复制出一个新数组,修改的操作在新数组中完成,最后将新数组交由array变量指向。
- 写加锁,读不加锁
4.3 迭代方法
public Iterator<E> iterator() {return new COWIterator<E>(getArray(), 0);
}//迭代器成员属性
//被迭代数组
private final Object[] snapshot;
//被返回元素的下标
private int cursor;private COWIterator(Object[] elements, int initialCursor) {cursor = initialCursor;snapshot = elements;
}
迭代器中的方法都是基于snapshot数组进行的,而snapshot是被传入的原数组,因此,CopyOnWriteArrayList在用迭代器遍历时,操作的都是原数组。
在多线程情况下,其他线程执行clear等加锁操作时,会创建一个副本数组,进行操作,不影响原数组。
4.4 缺点
- 内存占用:如果CopyOnWriteArrayList经常要增删改里面的数据的话,经常要执行add,set,remove方法,这是比较耗费内存的。
- 这几个方法都是要复制一个数组出来的
- 数据一致性:CopyOnWriteArrayList容器只能保证数据的最终一致性,不能保证数据的实时一致性
- 当线程A在迭代CopyOnWriteArrayList容器中的数据,此时线程B在迭代中进行了增删改操作,调用了setArray方法,数据已经修改了,但是线程A迭代出来的还是原有的数据。
5. CopyOnWritSet
? CopyOnWriteSet的底层就是CopyOnWriteArrayList
private final CopyOnWriteArrayList<E> al;public CopyOnWriteArraySet() {al = new CopyOnWriteArrayList<E>();
}