ArrayList 并发缺陷
ArrayList 相信大家都用过,我们来看看它在并发下的缺陷。
public static void main(String[] args) {
ArrayList<String> names = new ArrayList<>();
names.add("张三");
names.add("李四");
names.add("王五");
names.add("赵六");
Iterator<String> iterator = names.iterator();
while (iterator.hasNext()){
String name = iterator.next();
if (name.equals("王五")){
names.add("田七");
}
System.out.println(name);
}
}

在遍历集合的过程中,如果对集合做了修改,会抛出 ConcurrentModificationException 异常,在 Java 中,for 循环遍历集合会转换成 Iterator 遍历,我们来看看迭代器遍历时的检验机制。
// ArrayList 中的元素数量
private int size;
// 存储数据
transient Object[] elementData;
// 集合中元素的修改次数
protected transient int modCount = 0;
// 添加方法
public boolean add(E e) {
ensureCapacityInternal(size + 1); // modCount + 1
elementData[size++] = e;
return true;
}
private void ensureExplicitCapacity(int minCapacity) {
modCount++;
// 扩容,不是我们关注的重点
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
// ArrayList 中的迭代器
private class Itr implements Iterator<E> {
int cursor; // 下一个要返回的元素索引
int lastRet = -1; // 上一个返回的元素的索引; -1 if no such
int expectedModCount = modCount;
Itr() {}
public boolean hasNext() {
return cursor != size;
}
// 主要源码
public E next() {
checkForComodification();
int i = cursor;
Object[] elementData = ArrayList.this.elementData;
cursor = i + 1;
return (E) elementData[lastRet = i];
}
// 检验遍历过程中集合元素是否受到了修改
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
}
在遍历过程中,每次调用 next() 方法时,都会进行 checkForComodification(),检验expectedCount 和 modCount 是否相等,如果不相等则抛出 ConcurrentModificationException 异常。每次集合添加或删除元素时,modCount 就会 +1。
并发环境下,ArrayList 肯定是线程不安全的,当一个线程遍历时,其他线程对集合元素做了修改,就会报错。
CopyOnWriteArrayList
思想
CopyOnWriteArrayList 是一个线程安全的 ArrayList,对其进行的修改操作都是在一个复制的数组(快照)中进行,采用了写时复制策略。

主要成员变量: array 一个 Object 类型的数组,用来存放具体的元素。ReentrantLock 独占锁对象(后面再详解),保证了同一时间只有一个线程能进入到方法中。
// 独占锁对象
final transient ReentrantLock lock = new ReentrantLock();
private transient volatile Object[] array;
使用
public static void main(String[] args) {
CopyOnWriteArrayList<String> names = new CopyOnWriteArrayList<>();
names.add("张三");
names.add("李四");
names.add("王五");
names.add("赵六");
Iterator<String> iterator = names.iterator();
while (iterator.hasNext()){
String name = iterator.next();
if (name.equals("王五")){
names.add("田七");
}
System.out.println(name);
}
}
这段代码能正常的执行,下面我们来分析 CopyOnWriteArrayList 的主要方法的源码。
add()方法
// 获取 array 对象
final Object[] getArray() {
return array;
}
final void setArray(Object[] a) {
array = a;
}
public boolean add(E e) {
final ReentrantLock lock = this.lock;
lock.lock(); // 保证了同一时间只有一个线程能执行下面的代码
try {
Object[] elements = getArray();
// 复制 array 到新数组
int len = elements.length;
Object[] newElements = Arrays.copyOf(elements, len + 1);
newElements[len] = e;
// 新数组替换旧数组
setArray(newElements);
return true;
} finally {
lock.unlock(); // 释放锁
}
}
调用 add() 方法的线程先会获取独占锁,获取锁后复制 array 到一个新数组(新数组的大小是原来的大小 +1,可以知道 CopyOnWriteArrayList 是无界集合),最后用新数组替换旧数组,并释放锁。整个 add() 是原子过程,并且添加并不是在原来的数组是添加,而是在快照上添加(写时复制)。
修改和删除元素也是采用写时复制的策略,在新数组中进行修改操作,然后将新数组赋值给 array。
get()
public E get(int index) { // 没有加锁
return get(getArray(), index);
}
private E get(Object[] a, int index) {
return (E) a[index];
}
ArrayList 中 get() 方法时直接从数组里取值,但是这里是先把数组复制给变量 a,然后从 a 中取值,这样做有什么好处?
线程 X 调用 get() 方法获取指定元素可以分两步: 获取 array 数组(步骤 A),通过下标访问指定元素的位置(步骤 B )。整个过程没有加锁同步。
加锁在某个时刻,array 中的元素为 1,2,3。线程 X 执行 get(0) 方法。

在线程 X 执行完 A 步骤,执行 B 步骤前,另一个线程 Y 删除元素 1 ,并重写回 array 数组。这时候线程 X 还能取到元素 1 吗? 答案是当然可以,因为线程 X 访问的是临时数组 a,其他线程对 array 的修改对其不会造成影响。

iterator()
static final class COWIterator<E> implements ListIterator<E> {
// array 的快照
private final Object[] snapshot;
// 数组下标
private int cursor;
// 构造函数
private COWIterator(Object[] elements, int initialCursor) {
cursor = initialCursor;
snapshot = elements;
}
public boolean hasNext() {
return cursor < snapshot.length;
}
public E next() {
if (! hasNext())
throw new NoSuchElementException();
return (E) snapshot[cursor++];
}
}
对 array 的修改对 snapshot 是不可见的,当 array 修改后,array 和 snapshot 就是两个完全不同的数组了( 因为通过前面的源码可知, array 相当于又 new 了一个 )。所以使用迭代器时,其他线程对该集合的修改不可见,保证了线程安全。
检验
public static void main(String[] args) throws InterruptedException {
CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();
list.add("111");
list.add("222");
list.add("333");
Iterator<String> iterator = list.iterator();
Thread t1 = new Thread(()->{
list.add("444");
list.add("555");
});
t1.start();
t1.join(); // 等待线程 t1 执行完
while (iterator.hasNext()){
System.out.println(iterator.next());
}
}

这段代码再次检验了其他线程对集合的修改对 iterator 对象不可见。