前言:我们知道ArrayList是线程不安全的。要实现线程安全的List,我们可以使用Vector,或者使用Collections工具类将List包装成一个SynchronizedList。其实在Java并发包中还有一个CopyOnWriteArrayList可以实现线程安全的List。
1、Vector
Vector这个类是一个非常古老的类了,在JDK1.0的时候便已经存在,其实现安全的手段非常简单所有的方法都加上synchronized关键字,这样保证这个实例的方法同一时刻只能有一个线程访问,所以在高并发场景下性能非常低。
2、SynchronizedList
SynchronizedList是java.util.Collections中的一个静态内部类,其实现安全的手段稍微有一点优化,就是把Vector加在方法上的synchronized关键字,移到了方法里面变成了同步块而不是同步方法从而把锁的范围缩小了,另外,SynchronizedList中的方法不全都是同步的,比如获取迭代器方法listIterator()就不是同步的。
3、什么是COW机制?
Copy-On-Write简称COW。通俗的理解是当我们往一个容器添加元素的时候,不直接往当前容器添加,而是先将当前容器进行Copy,复制出一个新的容器,然后新的容器里添加元素,添加完元素之后,再将原容器的引用指向新的容器。这样做的好处是我们可以对CopyOnWrite容器进行并发的读,而不需要加锁,因为当前容器不会添加任何元素。所以CopyOnWrite容器也是一种读写分离的思想,读和写不同的容器。从JDK1.5开始Java并发包里提供了两个使用CopyOnWrite机制实现的并发容器,它们是CopyOnWriteArrayList和CopyOnWriteArraySet。
4、CopyOnWriteArrayList源码分析
CopyOnWrite容器即写时复制的容器。通俗的理解是当我们往一个容器添加元素的时候,不直接往当前容器添加,而是先将当前容器进行Copy,复制出一个新的容器,然后新的容器里添加元素,添加完元素之后,再将原容器的引用指向新的容器。CopyOnWriteArrayList中add/remove等写方法是需要加锁的,目的是为了避免Copy出N个副本出来,导致并发写。
但是,CopyOnWriteArrayList中的读方法是没有加锁的。这样做的好处是我们可以对CopyOnWrite容器进行并发的读,当然,这里读到的数据可能不是最新的。因为写时复制的思想是通过延时更新的策略来实现数据的最终一致性的,并非强一致性。
所以CopyOnWrite容器是一种读写分离的思想,读和写不同的容器。而Vector在读写的时候使用同一个容器,读写互斥,同时只能做一件事儿。
看下CopyOnWriteArrayList的成员变量
//重入锁保写操作互斥
final transient ReentrantLock lock = new ReentrantLock();
//volatile保证读可见性
private transient volatile Object[] array;
下面再看下添加元素的代码逻辑
public boolean add(E e) {
final ReentrantLock lock = this.lock;
lock.lock();//加锁
try {
Object[] elements = getArray();//读取原数组
int len = elements.length;
//构建一个长度为len+1的新数组,然后拷贝旧数据的数据到新数组
Object[] newElements = Arrays.copyOf(elements, len + 1);
//把新加的数据赋值到最后一位
newElements[len] = e;
// 替换旧的数组
setArray(newElements);
return true;
} finally {
lock.unlock();
}
}
先获得锁,然后拷贝元素组并将新元素加入(添加的元素可以是null),再替换掉原来的数组。我们会发现这种实现方式非常不适合频繁修改的操作。CopyOnWriteArrayList的删除和修改的操作的原理也是类似的,这边就不贴代码了。
CopyOnWriteArrayList中add/remove等写方法是需要加锁的,目的是为了避免Copy出N个副本出来,导致并发写。
最后看下读操作
//直接获取index对应的元素
public E get(int index) {return get(getArray(), index);}
private E get(Object[] a, int index) {return (E) a[index];}
从以上的增删改查中我们可以发现,增删改都需要获得锁,并且锁只有一把,而读操作不需要获得锁,支持并发。为什么增删改中都需要创建一个新的数组,操作完成之后再赋给原来的引用?这是为了保证get的时候都能获取到元素,如果在增删改过程直接修改原来的数组,可能会造成执行读操作获取不到数据。
CopyOnWriteArrayList在使用迭代器遍历的时候,操作的都是原数组!而其他线程对其修改后会生成一个新的数组,所以并不影响之前数组的遍历。
CopyOnWriteArrayLis的缺点
- 内存占用:如果CopyOnWriteArrayList经常要增删改里面的数据,经常要执行add()、set()、remove()的话,那是比较耗费内存的。因为我们知道每次add()、set()、remove()这些增删改操作都要复制一个数组出来;
- 数据一致性:CopyOnWrite容器只能保证数据的最终一致性,不能保证数据的实时一致性;从上面的例子也可以看出来,比如线程A在迭代CopyOnWriteArrayList容器的数据,线程B在线程A迭代的间隙中将CopyOnWriteArrayList部分的数据修改了(已经调用setArray()了),但是线程A迭代出来的是原有的数据。
5、CopyOnWriteArrayLis使用场景
整体来说CopyOnWriteArrayList是另类的线程安全的实现,但并一定是高效的,适合用在读取和遍历多的场景下,并不适合写并发高的场景,因为数组的拷贝也是非常耗时的,尤其是数据量大的情况下。
稍微总结下:
-
CopyOnWriteArrayList基于可重入锁机制,增删改操作需要加锁,读操作不需要加锁;
-
CopyOnWriteArrayList适合用在读取和遍历多的场景下,并不适合写并发高的场景;
-
基于fail-safe机制,不会抛出CurrentModifyException;
-
另外CopyOnWriteArrayList提供了弱一致性的迭代器,从而保证在获取迭代器后,其他线程对list的修改是不可见的,迭代器遍历的数组是一个快照。