之前分析过ArrayList的源码,发现ArrayList是线程不安全的,现在来解析CopyOnWriteArrayList的源码,看看它是怎么实现线程安全的。
创建
CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();
这是默认创建的方法,来看看源码。
//1
private transient volatile Object[] array;
final void setArray(Object[] a) {
array = a;
}
public CopyOnWriteArrayList() {
//2
setArray(new Object[0]);
}
和ArrayList一样,CopyOnWriteArrayList其实是用数组来存储元素的。不同的是CopyOnWriteArrayList的数组多了volatile关键字(代码1)。
首先要明白volatile的作用是什么,总的来说volatile有三大特性。
1.保证可见性
2.不保证原子性
3.禁止指令重排序
简单地说一下可见性,线程修改主内存的变量并不是直接在上面写的,而是先在线程自己的工作内存里创建一个副本,修改完在写会主内存。只有一个线程操作这个变量并没有什么问题,但多线程就不一样了,各线程并不能保证及时知道这个变量被其他线程改动过,也就是说变量的可见性得不到保证(对volatile还很陌生的朋友可以先看看这篇文章)。
回到源码(代码2),可以看到array是指向一个大小为0的数组的,也就是说在默认情况下CopyOnWriteArrayList的初始容量为0。再看看其他的创建方法。
public CopyOnWriteArrayList(Collection<? extends E> c) {
Object[] elements;
if (c.getClass() == CopyOnWriteArrayList.class)
elements = ((CopyOnWriteArrayList<?>)c).getArray();
else {
elements = c.toArray();
// c.toArray might (incorrectly) not return Object[] (see 6260652)
if (elements.getClass() != Object[].class)
elements = Arrays.copyOf(elements, elements.length, Object[].class);
}
setArray(elements);
}
public CopyOnWriteArrayList(E[] toCopyIn) {
setArray(Arrays.copyOf(toCopyIn, toCopyIn.length, Object[].class));
}
代码都很简单,就是让array指向元素数组的一个副本。仔细的朋友会发现,不同于ArrayList,CopyOnWriteArrayList并没有设置初始大小的构造方法。
添加元素
list.add("str1");
这是常用的方法,看看源码。
public boolean add(E e) {
final ReentrantLock lock = this.lock;
//1.1
lock.lock();
try {
Object[] elements = getArray();
int len = elements.length;
//2
Object[] newElements = Arrays.copyOf(elements, len + 1);
newElements[len] = e;
setArray(newElements);
return true;
} finally {
//1.2
lock.unlock();
}
}
看到代码1.1和1.2,分别是上锁和释放锁,这就是和ArrayList最大的区别,CopyOnWriteArrayList增加了锁机制。
现在来说说原子性,原子性是指一个操作是不可以再被分割的。比如,某原子操作分为A、B和C三个步骤,ABC要么全做完,要么全不做,不可以先做A再做D,然后再回去做BC。上面提到过volatile并不能保证操作的原子性。
从源码中可以看出CopyOnWriteArrayList是用lock和unlock(显式锁)来实现原子性的。那为什么不直接在add方法上添加synchronized(隐式锁)关键字呢?如果只是考虑原子性,synchronized方法完全可以胜任,但是synchronized加在方法上面意味着锁的作用范围是整个方法。而lock和unlock的可以根据需要锁定部分的代码块,相比之下灵活许多。
例如一个方法有3行代码是A->B->C,其中B和C必须是原子操作。如果采用synchronized方法的方式,那么线程运行到A时就可能会被阻塞了。而采用lock和unlock的方式把BC锁起来,那么线程可以运行完A再阻塞。这么一来就会发现,synchronized方法使用方便但会对性能有所损耗。
保证原子性就讲完了,那么add方法是怎么实现添加元素的呢?看到刚刚的代码2。创建一个新的数组,长度为当前长度加1,插入元素,再让array指向这个数组。
ArrayList每次添加元素都需要进行剩余容量检查。CopyOnWriteArrayList就不同了,每次调用add方法就得创建一个新的数组,也就没有容量检查和扩容的说法了。
删除元素
list.remove(0);
这是删除操作的基本用法,看看源码。
public E remove(int index) {
final ReentrantLock lock = this.lock;
//1.1
lock.lock();
try {
Object[] elements = getArray();
int len = elements.length;
E oldValue = get(elements, index);
int numMoved = len - index - 1;
if (numMoved == 0)
//2
setArray(Arrays.copyOf(elements, len - 1));
else {
//3
Object[] newElements = new Object[len - 1];
System.arraycopy(elements, 0, newElements, 0, index);
System.arraycopy(elements, index + 1, newElements, index,
numMoved);
setArray(newElements);
}
return oldValue;
} finally {
//1.2
lock.unlock();
}
}
同样,为了保证删除的原子性,在代码1.1和1.2分别使用了lock和unlock。代码2和代码块3也是一样创建一个删除后的数组,再让array指向它。
清空元素
public void clear() {
final ReentrantLock lock = this.lock;
lock.lock();
try {
setArray(new Object[0]);
} finally {
lock.unlock();
}
}
一样的,通过lock和unlock实现同步,再让array指向大小为0的数组(注意区别大小为0的数组和null)。