对于ArrayList来说,它的线程是不安全的。而Vector作为线程安全的list实现类,它的add、remove还是get方法都加上了synchronized锁,需要巨大的系统开销,效率低下。从JDK1.5开始Java并发包里提供了两个使用CopyOnWrite机制实现的并发集合容器,它们是CopyOnWriteArrayList和CopyOnWriteArraySet。
本文将对CopyOnWriteArrayList中的核心源码进行解读,了解其工作原理及思想体现。
Copy-On-Write简称COW,是一种用于集合的并发访问的优化策略。我们称之为:写时复制容器。基本思想是:当我们往一个集合容器中写入元素时(添加、修改、删除),并不会直接在集合容器中写入,而是先将当前集合容器进行Copy,复制出一个新的容器,然后新的容器里写入元素,写入操作完成之后,再将原容器的引用指向新的容器。
CopyOnWriteArrayList源码
public class CopyOnWriteArrayList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable {}
可以看出CopyOnWriteArrayList实现了List接口,RandomAccess接口(随机访问 根据下标),Cloneable接口(可以进行克隆),Serializable接口(可序列化)
//加锁 transient 不被序列化
final transient ReentrantLock lock = new ReentrantLock();
//array数组 volatile 轻量级的同步机制 开销更低 同步性更差
private transient volatile Object[] array;
/**
* Gets the array. Non-private so as to also be accessible
* from CopyOnWriteArraySet class.
*/
final Object[] getArray() {
return array;
}
/**
* Sets the array.
*/
final void setArray(Object[] a) {
array = a;
}
//创建空列表
public CopyOnWriteArrayList() {
setArray(new Object[0]);
}
volatile 修饰的是数组引用!简单的在原来数组修改几个元素的值,这种操作是无法发挥可见性的,必须通过修改数组内存地址
//get获取指定下标元素
@SuppressWarnings("unchecked")
private E get(Object[] a, int index) {
return (E) a[index];
}
public E get(int index) {
//调用内部get方法
return get(getArray(), index);
}
get(int index)不需要加锁,因为CopyOnWriteArrayList在add/remove操作时,不会修改原数组,所以读操作不会存在线程安全问题。这其实就是读写分离的思想,只有写入的时候才加锁,复制副本来进行修改
set方法
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;
// 拷贝新的数组
Object[] newElements = Arrays.copyOf(elements, len);
//根据索引修改元素
newElements[index] = element;
// 将原数组的引用指向新数组
setArray(newElements);
} else {
// Not quite a no-op; ensures volatile write semantics
//为了确保 voliatile 的语义,所以尽管写操作没有改变数据,还是调用set方法
setArray(elements);
}
return oldValue;
} finally {
lock.unlock();
}
}
set方法会加上锁,而get方法不加锁,这个时候如果多线程正好写数据,读取的时候还是会读取到旧的数据
add方法
//从尾部增加
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();
}
}
//指定下标 增加
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];
// 拷贝index之前的元素到新数组,拷贝前后,元素下标不变
System.arraycopy(elements, 0, newElements, 0, index);
// 拷贝index之后的元素到新数组,拷贝之后,下标+1
// 数组index处需要空出来留给新增元素
System.arraycopy(elements, index, newElements, index + 1,
numMoved);
}
newElements[index] = element;
// 新数组替换原数组
setArray(newElements);
} finally {
// 解锁
lock.unlock();
}
}
这两个add方法完成的功能不一样,但是实现步骤和原理都差不多
remove方法
//删除(根据下标)
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];
// 拷贝index之前的元素到新数组,拷贝前后下标不变
System.arraycopy(elements, 0, newElements, 0, index);
// 拷贝index之后的元素到新数组,拷贝之后下标-1
System.arraycopy(elements, index + 1, newElements, index,
numMoved);
// 新数组替换原数组
setArray(newElements);
}
// 返回删除的值
return oldValue;
} finally {
// 解锁
lock.unlock();
}
}
//随机删除
void removeRange(int fromIndex, int toIndex) {
//加锁
final ReentrantLock lock = this.lock;
lock.lock();
try {
//获取原数组
Object[] elements = getArray();
int len = elements.length;
//原元素下标小于0 新元素下标超出长度 新元素下标小于原元素下标 抛出异常
if (fromIndex < 0 || toIndex > len || toIndex < fromIndex)
throw new IndexOutOfBoundsException();
//移动长度
int newlen = len - (toIndex - fromIndex);
// 需要移动的元素的个数
int numMoved = len - toIndex;
if (numMoved == 0)
//删除后
setArray(Arrays.copyOf(elements, newlen));
else {
// 新数组
Object[] newElements = new Object[newlen];
// 拷贝index之前的元素到新数组
System.arraycopy(elements, 0, newElements, 0, fromIndex);
// 拷贝index之后的元素到新数组
System.arraycopy(elements, toIndex, newElements,
fromIndex, numMoved);
// 新数组替换原数组
setArray(newElements);
}
} finally {
// 解锁
lock.unlock();
}
}
Vector、ArrayList、CopyOnWriteArrayList
这三个集合类都继承List接口
1、ArrayList是线程不安全的
2、Vector是比较古老的线程安全的,但性能不行
3、CopyOnWriteArrayList在兼顾了线程安全的同时,又提高了并发性,性能比Vector要高
CopyOnWriteArrayList有什么优缺点
缺点:
1、内存占用,因为写时复制的原理,所以在添加新元素的时候会复制一份,此刻内存中就会有两份对象
2、数据一致性问题,因为CopyOnWrite容器只能保证最终的数据一致性,并不能保证数据的实时性,也就是不具备原子性的效果。
3、效率低,随着数组的元素越来越多,修改的时候拷贝数组将会越来越耗时。
优点:
1、读多写少,很多时候我们的系统应对的都是读多写少的并发场景,读操作是无锁操作所以性能较高