大家都知道ArrayList是线程不安全的,推荐我们自己加锁或者使用Collections.synchronizedList方法,其实JDK还提供了一种线程安全的List-CopyOnWriteArrayList,它的特征如下:
1、 线程安全
2、 通过锁+数组拷贝+volatile关键字保证了线程安全
3、 每次数组操作,都会把数组拷贝一份出来,在新数组上进行操作,操作成功之后再赋值回去
整体思路
从整体讲,CopyOnWriteArrayList数据结构和ArrayList是一致的,底层是个数组
CopyOnWriteArrayList对数组进行操作时,会进行一下操作
1、加锁
2、从原数组中拷贝出新数组
3、在新数组上进行操作,并把新数组赋值给数组容器
4、解锁
另外CopyOnWriteArrayList底层数组采用volatile修饰,保证了可见性
不知道看到这里你是否和我有同样的疑问,既然加锁了,为什么还要加volatile关键字?
读到后面发现CopyOnWriteArrayList读操作并不会加锁,也就是不同线程操作可能拿到不同的锁(或者说有的不拿到锁),也就没法保证happens-before原则,volatile可以保证happens-before语意
新增
新增有很多种情况,比如新增到数组尾部、新增到数组某一个索引位置、批量新增等
首先是新增到数组尾部,源码如下:
public boolean add(E e) {
final ReentrantLock lock = this.lock;
lock.lock();
try {
Object[] elements = getArray();
int len = elements.length;
Object[] newElements = Arrays.copyOf(elements, len + 1);
newElements[len] = e;
setArray(newElements);
return true;
} finally {
lock.unlock();
}
}可以看到,整个过程可以分为五步,加锁、新建数组拷贝原来数组、插入数据、给原来数组赋值、解锁
不知道看到这里你是否和我有同样的疑问,为什么要拷贝一份数据出来呢?
这里其实是和volatile有关,因为volatile修饰的是数组,线程的工作内存只拷贝了数组的引用,如果只改变数组值,无法触发可见性
再看指定位置插入数据
public void add(int index, E element) {
final ReentrantLock lock = this.lock;
lock.lock();
try {
Object[] elements = getArray();
int len = elements.length;
if (index > len || index < 0)
throw new IndexOutOfBoundsException("Index: "+index+
", Size: "+len);
Object[] newElements;
int numMoved = len - index;
if (numMoved == 0)
newElements = Arrays.copyOf(elements, len + 1);
else {
newElements = new Object[len + 1];
System.arraycopy(elements, 0, newElements, 0, index);
System.arraycopy(elements, index, newElements, index + 1,
numMoved);
}
newElements[index] = element;
setArray(newElements);
} finally {
lock.unlock();
}
}可以看到,如果插入位置正好是末尾,只需要拷贝一次。否则,会进行两次拷贝
删除
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;
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();
}
}可以看得出,删除操作与新增操作类似。分为四步
1、加锁
2、判断索引位置,选择不同策略拷贝数组
3、给原来数组赋值
4、解锁
批量删除
源码如下:
public boolean removeAll(Collection<?> c) {
if (c == null) throw new NullPointerException();
final ReentrantLock lock = this.lock;
lock.lock();
try {
Object[] elements = getArray();
int len = elements.length;
if (len != 0) {
// temp array holds those elements we know we want to keep
int newlen = 0;
Object[] temp = new Object[len];
for (int i = 0; i < len; ++i) {
Object element = elements[i];
if (!c.contains(element))
temp[newlen++] = element;
}
if (newlen != len) {
setArray(Arrays.copyOf(temp, newlen));
return true;
}
}
return false;
} finally {
lock.unlock();
}
}从源码中可以看到,批量删除并不会直接对数组中得到元素挨个删除,而是先对数组中值进行循环判断,把不需要删除的放到临时数组中,最后临时数组中的数据就是我们不需要删除的数据(个人觉得这是一种空间换时间的思想,因为删除操作需要o(n)的时间复杂度)
迭代
CopyOnWriteArrayList,数组原值(包括增加和删除)被改变也不会抛出ConcurrentModificationException异常,因为其迭代器持有的老数组的引用,每次都是拷贝出新数组,不会影响老数组。

京公网安备 11010502036488号