官方简介:
ArrayList的线程安全变体,其中所有可变操作(add、set等)都是通过制作底层数组的【新副本】来实现的。
这通常成本太高,但当遍历操作的数量远远超过突变时,可能比其他方法更高效,当您不能或不想同步遍历,但需要排除并发线程之间的干扰时,这很有用。“快照”样式的迭代器方法使用对创建迭代器时数组状态的引用。此数组在迭代器的生存期内从不更改,因此不可能发生干扰,并且迭代器保证不会抛出ConcurrentModificationException。
迭代器不会反映自创建迭代器以来对列表的添加、删除或更改。不支持迭代器本身的元素更改操作(移除、设置和添加)。这些方法抛出不支持OperationException。
允许所有元素,包括null。
【内存一致性影响】:与其他并发集合一样,在将对象放入CopyOnWriteArrayList线程中的操作发生在另一个线程中从CopyOnWriteArray列表访问或删除该元素之后的操作之前。
ArrayList的线程安全变体,所以说我们需要学习的是为什么CopyOnWriteList线程安全。
主要看下面几个问题:
- add线程安全
- set线程安全
- remove线程安全
- 未加锁的get线程安全
源码解析
1. add线程安全
/**
* Appends the specified element to the end of this list.
*
* @param e element to be appended to this list
* @return {@code true} (as specified by {@link Collection#add})
*/
public boolean add(E e) {
// 加锁,保证线程安全
final ReentrantLock lock = this.lock;
lock.lock();
try {
Object[] elements = getArray();
int len = elements.length;
// 注意这里copy了一份新的list,list的大小比旧的大1
// copy过程中不会发生线程安全问题,因为所有的对list修改的操作都共用一把锁
Object[] newElements = Arrays.copyOf(elements, len + 1);
newElements[len] = e;
// 置换旧的list
setArray(newElements);
return true;
} finally {
lock.unlock();
}
}
2. set线程安全
/**
* ÷ the element at the specified position in this list with the
* specified element.
*
* @throws IndexOutOfBoundsException {@inheritDoc}
*/
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;
// 如果将要set的元素和当前位置上的元素不想等
// 仍然copy一份新的list,在它上面操作
Object[] newElements = Arrays.copyOf(elements, len);
newElements[index] = element;
// 操作之后置换
setArray(newElements);
} else {
// Not quite a no-op; ensures volatile write semantics
// 相等说明不需要换
setArray(elements);
}
return oldValue;
} finally {
lock.unlock();
}
}
3. remove线程安全
/**
* Removes the element at the specified position in this list.
* Shifts any subsequent elements to the left (subtracts one from their
* indices). Returns the element that was removed from the list.
*
* @throws IndexOutOfBoundsException {@inheritDoc}
*/
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;
// 可以看到仍然是copy新的list,然后在新的list操作,最终置换list
if (numMoved == 0)
setArray(Arrays.copyOf(elements, len - 1));
else {
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 {
lock.unlock();
}
}
4. 未加锁的get线程安全
public E get(int index) {
return get(getArray(), index);
}
final Object[] getArray() {
return array;
}
// 注意这里为什么没有直接用array,而是作为参数
private E get(Object[] a, int index) {
return (E) a[index];
}
可以看到并没有加锁,【因为所有的操作都是在list的副本操作的,所以不需要加锁】。
一个小问题
get(Object[] a, int index)
方法体为什么不直接用成员变量array,而是通过参数传进去?
答:个人认为:
- 对于调用者而言,想要的是调用get那一刻array里面存储的数据,按照当前的写法,程序拿到了调用那一刻的array引用;如果直接用array的话,那么在get期间,可能array引用的值会被其他的修改方法给修改了,那么得到的是新的array的值,不一定符合预期
- 符合happens-before语意:将一个元素放入一个线程安全容器的操作Happens-Before从容器中取出这个元素的操作(虽然add操作在最后才将新的array赋值给成员变量array,但是后面还有return、unlock操作,从程序语意上来说,add并没有执行完)
总结
- 其实现原理采用”CopyOnWrite”的思路(不可变元素),即所有写操作,包括:add,remove,set等都会触发底层数组的拷贝,从而在写操作过程中,不会影响读操作;避免了使用synchronized等进行读写操作的线程同步;
- 写加锁同时还进行了copy,所以说 CopyOnWrite对于写操作来说代价很大,故不适合于写操作很多的场景;当遍历操作远远多于写操作的时候,适合使用CopyOnWriteArrayList;
- 迭代器以”快照”方式实现,在迭代器创建时,引用指向List当前状态的底层数组,所以在迭代器使用的整个生命周期中,其内部数据不会被改变;并且集合在遍历过程中进行修改,也不会抛出ConcurrentModificationException;迭代器在遍历过程中,不会感知集合的add,remove,set等操作;
- 因为迭代器指向的是底层数组的”快照”,因此也不支持对迭代器本身的修改操作,包括add,remove,set等操作,如果使用这些操作,将会抛出UnsupportedOperationException;
- 相关Happens-Before规则:一个线程将元素放入集合的操作happens-before于其它线程访问/删除该元素的操作;
适用场景
数据量较小,读操作尤其是遍历操作【远多于】写操作时候,适合使用CopyOnWriteArrayList。