基本思想是:当我们往一个集合容器中写入元素时(添加、修改、删除),并不会直接在集合容器中写入,而是先将当前集合容器进行Copy,复制出一个新的容器,然后新的容器里写入元素,写入操作完成之后,再将原容器的引用指向新的容器。
这样做的好处:实现对 CopyOnWrite集合容器写入操作时的线程安全,但同时并不影响进行并发的读取操作,读写分离。从JDK1.5 开始Java 并发包里提供了两个使用CopyOnWrite机制实现的并发集合容器,它们是CopyOnWriteArrayList和 CopyOnWriteArraySet。
CopyOnWriteArrayList 相当于线程安全的ArrayList,内部存储结构采用Object[]数组,线程安全使用 ReentrantLock 实现,允许多个线程并发读取,但只能有一个线程写入。
public class CopyOnWriteArrayList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable {
// 定义了一个锁,即对写入(增加,删除,修改)使用的,
// 保护写入时线程安全,因为只有一把锁,所以有一个线程在进行写入操作时,
// 其他的写入操作也不能被其他线程使用
final transient ReentrantLock lock = new ReentrantLock();
private transient volatile Object[] array;
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;
//根据原数组Arrays工具类copyOf()方法复制一个新的数组,数组长度与原数组长度一致
Object[] newElements = Arrays.copyOf(elements, len);
//给新数组的目标索引位置添加新的元素
newElements[index] = element;
setArray(newElements);
} else {
//如果需要添加的元素在原数组中存在,则直接赋值给新数组
setArray(elements);
}
return oldValue;
} finally {
lock.unlock(); //手动释放锁
}
}
add()方法
public boolean add(E e) {
//上锁操作,保证写操作的原子性
final ReentrantLock lock = this.lock;
lock.lock();
try {
//getArray()方法是获取到内部维护的数组
Object[] elements = getArray();
// 获取数组的长度
int len = elements.length;
// 通过Arrays工具类的copyOf()方法复制出一个新的数组,并且长度+1
Object[] newElements = Arrays.copyOf(elements, len + 1);
// 新数组的最后一位的赋要增加的值
newElements[len] = e;
// 将新数组赋值到内部维护的数组中
setArray(newElements);
return true;
} finally {
lock.unlock(); //手动释放锁
}
}
add()重载方法
public void add(int index, E element) {
final ReentrantLock lock = this.lock;
lock.lock();
try {
Object[] elements = getArray();
int len = elements.length;
//判断要添加新元素的索引位置是否超过数组长度或则小于0
if (index > len || index < 0)
//如果超过则抛出异常
throw new IndexOutOfBoundsException("Index: "+index+
", Size: "+len);
Object[] newElements;
int numMoved = len - index;
//如果没超过就再判断是否是数组长度最后一位的索引
if (numMoved == 0)
//是最后一位就复制一个新的数组并长度加1
newElements = Arrays.copyOf(elements, len + 1);
else {
//不是最后一位就将原有数组的索引位置前所有的内容复制到新数组
//再将原有数组索引位置后所有的内容添加到新数组的索引位置+1位置的后面
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();
}
}
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)
//如果是最后以为就复制一个新的数组并长度-1
setArray(Arrays.copyOf(elements, len - 1));
else {
//如果不是最后一位,则需要新建一个原数组长度-1的新数组
//将原数组从索引位置0开始复制到目标索引位置给新数组
//再将原数组中目标索引位置+1开始复制到新数组的目标位置后面
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();
}
}
removeRange()方法
//删除指定索引范围内的数据
void removeRange(int fromIndex, int toIndex) {
final ReentrantLock lock = this.lock;
lock.lock();
try {
Object[] elements = getArray();
int len = elements.length;
//判断开始索引位置和结束索引位置是否复合逻辑
//结束索引位置是否大于数组长度,开始索引位置是否大于结束索引位置
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 {
//如果不是最后一位,则要建一个新的数组
//从原数组的索引位置0开始复制到新数组中(索引位置也从0开始)
//再将原数组中从结束索引位置开始复制到新数组的开始索引位置
Object[] newElements = new Object[newlen];
System.arraycopy(elements, 0, newElements, 0, fromIndex);
System.arraycopy(elements, toIndex, newElements,
fromIndex, numMoved);
setArray(newElements);
}
} finally {
lock.unlock();
}
}
removeAll()方法
//删除数组中c中含有的数据
public boolean removeAll(Collection<?> c) {
//如果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) {
int newlen = 0;
Object[] temp = new Object[len];
//将原数组的元素依次遍历
//如果C中不包含该元素,便将该元素添加到新建的数组中
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; //如果原数组为空返回false
} finally {
lock.unlock();
}
}
总结
- CopyOnWriteArrayList体现了写时复制的思想,增删改操作都是在复制的新数组中进行的。
- CopyOnWriteArrayList的增删改方法通过可重入锁确保线程安全;
- CopyOnWriteArrayList线程安全体现在多线程增删改不会抛出
java.util.ConcurrentModificationException
异常,并不能确保数据的强一致性。 - 同一时刻只能有一个线程对CopyOnWriteArrayList进行增删改操作,而读操作没有限制,并且 CopyOnWriteArrayList增删改操作都需要复制一份新数组,增加了内存消耗,所以CopyOnWriteArrayList适合读多写少的情况。