从名字就可以看出来CopyOnWriteArrayList和ArrayList是有关联的。ArrayList是线程非安全的,多线程时官方推荐我们使用 "Collections.synchronizedList(new ArrayList(...))"或者自己加锁,其实Java还提供了一种线程安全的ArrayList,这个线程安全的List就是我今天所写的CopyOnWriteArrayList。
结构
CopyOnWriteArrayList底层和ArrayList一样都是数组。前者的数组被volatile关键字修饰,表示数组的"内存地址"一旦被修改,其他线程能立马感知到。当我们对它进行增删的操作时他会加锁,拷贝出新数组,在新数组上进行操作,更改好之后把新数组赋值给数组容器,最后解锁。注意,它只对写操作进行加锁,读没有加锁。它只是线程安全的,并不能一定得到实时的数据。
//锁
//性能上ReentrantLock和synchronized没有什么区别,
//但ReentrantLock相比synchronized而言功能更加丰富,
//使用起来更为灵活,也更适合复杂的并发场景
final transient ReentrantLock lock = new ReentrantLock();
//使用volatile修饰的数组容器
private transient volatile Object[] array;
类注释
1:ArrayList的线程安全变体。其中所有的可变操作(add,remove,set等)都是通过创建底层数组的新副本来实现的。
2:允许存储所有元素 ,包括null。
3:迭代过程中不会抛出ConcurrentModificationException异常。
方法
add,remove和set方法基本分四步走: 1:通过ReentrantLoct加锁,保证同一时刻数组只能被一个线程操作。 2:通过Arrays.copyOf拷贝出新数组。 3:在新数组上进行操作,并把新数组赋值给数组容器,保证数组的内存地址被修改。volatile监控的是内存的地址。 4:在finally里解锁,即使异常也能释放琐。
add(E)
//使用volatile修饰的数组容器
private transient volatile Object[] array;
// 从尾部添加元素
public boolean add(E e) {
final ReentrantLock lock = this.lock;
// 1:通过ReentrantLoct加锁,保证同一时刻数组只能被一个线程操作。
lock.lock();
try {
//原数组
Object[] elements = getArray();
int len = elements.length;
//2:通过Arrays.copyOf拷贝出新数组,新数组的长度是 + 1 的,因为新增会多一个元素
Object[] newElements = Arrays.copyOf(elements, len + 1);
//3:在新数组上进行操作,元素直接添加到数组的尾部
newElements[len] = e;
//并把新数组赋值给数组容器,保证数组的内存地址被修改,volatile监控的是内存的地址。
setArray(newElements);
return true;
//4:在finally里解锁 ,即使异常也能释放琐。
} finally {
lock.unlock();
}
}
final void setArray(Object[] a) {
array = a;
}
remove(int)
// 删除指定下标位置的元素
public E remove(int index) {
final ReentrantLock lock = this.lock;
//1:通过ReentrantLoct加锁,保证同一时刻数组只能被一个线程操作。
lock.lock();
try {
Object[] elements = getArray();
int len = elements.length;
//得到老值
E oldValue = get(elements, index);
int numMoved = len - index - 1;
if (numMoved == 0)//删除的数据是数组的尾部,拷贝范围0-(len-1)
//2:通过Arrays.copyOf拷贝出新数组,
//3:在新数组上进行操作
setArray(Arrays.copyOf(elements, len - 1));
else {
//2:通过Arrays.copyOf拷贝出新数组,
//3:在新数组上进行操作
// 删除的数据在数组的中间,分三步走
// 1:设置新数组的长度减一,因为是减少一个元素
// 2:从 0 拷贝到数组新位置
// 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 {//4:在finally里解锁 ,即使异常也能释放琐。
lock.unlock();
}
}
set(int,E)
//替换指定下标位置的元素
public E set(int index, E element) {
final ReentrantLock lock = this.lock;
1:通过ReentrantLoct加锁,保证同一时刻数组只能被一个线程操作。
lock.lock();
try {
Object[] elements = getArray();
//得到旧元素
E oldValue = get(elements, index);
if (oldValue != element) {//旧值和新值不一样
int len = elements.length;
// 2:通过Arrays.copyOf拷贝出新数组。
Object[] newElements = Arrays.copyOf(elements, len);
//3:在新数组上进行操作,并把新数组赋值给数组容器,保证数组的内存地址被修改。volatile监控的是内存的地址。
newElements[index] = element;
setArray(newElements);
} else {//旧值和新值一样
// Not quite a no-op; ensures volatile write semantics
setArray(elements);
}
return oldValue;
//4:在finally里解锁,即使异常也能释放琐。
} finally {
lock.unlock();
}
}
迭代
迭代过程不会快速故障,是因为迭代过程中使用的是旧数组的引用。旧数组的结构不会发生变化。
public Iterator<E> iterator() {
return new COWIterator<E>(getArray(), 0);
}
final void setArray(Object[] a) {
array = a;//这里更改的只是array的指向的内存地址,并没有更改旧数组的结构。
}
static final class COWIterator<E> implements ListIterator<E> {
...
private final Object[] snapshot;
private COWIterator(Object[] elements, int initialCursor) {
cursor = initialCursor;
snapshot = elements;//得到旧数组的引用
}
...
}
CopyOnWriteArrayList对写加锁,读不加锁,我们加锁的时候可以借鉴一下。使用时我们更多的用于读操作多于写操作的场景。
CopyOnWriteArrayList只是保证线程安全,并不保证你得到或操作的数据是最新的。