Copy-On-Write
简称COW
,是一种用于集合的并发访问的优化策略。基本思想是:当我们往一个集合容器中写入元素时(添加、修改、删除),并不会直接在集合容器中写入,而是先将当前集合容器进行Copy,复制出一个新的容器,然后新的容器里写入元素,写入操作完成之后,再将原容器的引用指向新的容器。这样做的好处:实现对
CopyOnWrite
集合容器写入操作时的线程安全,但同时并不影响进行并发的读取操作。所以CopyOnWrite
容器也是一种读写分离的思想。从JDK1.5
开始Java
并发包里提供了两个使用CopyOnWrite
机制实现的并发集合容器,它们是CopyOnWriteArrayList
和CopyOnWriteArraySet
。
CopyOnWriteArrayList
相当于线程安全的ArrayList
,内部存储结构采用Object[]
数组,线程安全使用ReentrantLock
实现,允许多个线程并发读取,但只能有一个线程写入。
add(E e)方法:
添加新元素至集合时,会将当前数组Copy
复制新数组,并将新元素添加至新数组,最后替换原数组。执行过程中,使用ReentrantLock
加锁,保证线程安全,避免多个线程复制数组。
// 添加元素
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();// 最后在finally中释放锁
}
}
add(int index, E element)方法:
将元素添加到指定下标位置。
// 将元素添加到指定位置
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)// 判断指定下标是否越界,如果越界抛出IndexOutOfBountsException异常
throw new IndexOutOfBoundsException("Index: "+index+
", Size: "+len);
Object[] newElements;// 定义一个新数组
int numMoved = len - index;// 得到从该下标到数组末尾的长度
if (numMoved == 0)// 当numMoved==0时,意味着要添加在数组末尾
newElements = Arrays.copyOf(elements, len + 1);// 将当前数组长度加一复制到一个新数组中
else {
newElements = new Object[len + 1];// 将新数组的长度设置为当前数组长度加一
System.arraycopy(elements, 0, newElements, 0, index);//利用Systems工具类中的方法将旧数组复制到新数组中
System.arraycopy(elements, index, newElements, index + 1,//利用Systems工具类中的方法将旧数组复制到新数组中
numMoved);
}
newElements[index] = element;// 将本次要添加的元素存到指定位置
setArray(newElements);// 将新数组存回去
} finally {
lock.unlock();// 最后在finally中释放锁
}
}
小结:
从 add 系列方法可以看出,CopyOnWriteArrayList 通过加锁 + 数组拷贝+ volatile 来保证了线程安全,每一个要素都有着其独特的含义:
1.加锁:保证同一时刻数组只能被一个线程操作;
2.数组拷贝:保证数组的内存地址被修改,修改后触发 volatile 的可见性,其它线程可以立马知道数组已经被修改;
3.volatile:值被修改后,其它线程能够立马感知最新值。
三个个要素缺一不可,比如说我们只使用 1 和 3 ,去掉 2,这样当我们修改数组中某个值时,并不会触发 volatile 的可见特性的,只有当数组内存地址被修改后,才能触发把最新值通知给其他线程的特性。
remove(int index)方法:
删除指定下标元素。根据指定下标,从原数组中,Copy
复制其它元素至新数组,最后替换原数组。
// 通过指定下标删除元素
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);// 通过get方法将数组和指定下标传进去返回值为要删除的元素
int numMoved = len - index - 1;// 得到从该下标到数组末尾的长度
if (numMoved == 0)// 当numMoved==0时,意味着要删除的元素在数组末尾
setArray(Arrays.copyOf(elements, len - 1));// 将当前数组长度减一复制到一个新数组中
else {
Object[] newElements = new Object[len - 1];// 将新数组的长度设置为当前数组长度减一
System.arraycopy(elements, 0, newElements, 0, index);//利用Systems工具类中的方法将旧数组复制到新数组中
System.arraycopy(elements, index + 1, newElements, index,//利用Systems工具类中的方法将旧数组复制到新数组中
numMoved);
setArray(newElements);// 将新数组存回去
}
return oldValue;// 返回要删除的元素
} finally {
lock.unlock();// 最后在finally中释放锁
}
}
removeRange(int fromIndex, int toIndex)方法:
删除指定区间元素
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();// 如果越界抛出IndexOutOfBoundsException异常
int newlen = len - (toIndex - fromIndex);// 得到删除后新数组的长度
int numMoved = len - toIndex;// 得到从该下标到数组末尾的长度
if (numMoved == 0) // 如果numMoved == 0意味着原数组全部都会删除
setArray(Arrays.copyOf(elements, newlen));
else {
Object[] newElements = new Object[newlen]; // 创建一个新数组,指定长度为newlen
System.arraycopy(elements, 0, newElements, 0, fromIndex);//利用Systems工具类中的方法将旧数组复制到新数组中
System.arraycopy(elements, toIndex, newElements,//利用Systems工具类中的方法将旧数组复制到新数组中
fromIndex, numMoved);
setArray(newElements);// 将新数组存回去
}
} finally {
lock.unlock();// 最后在finally中释放锁
}
}
removeAll(Collection<?> c)方法:
批量删除包含在 c 中的元素
// 批量删除包含在 c 中的元素
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;
// 说明数组有值,数组无值直接返回 false
if (len != 0) {
// newlen 表示新数组的索引位置,新数组中存在不包含在 c 中的元素
int newlen = 0;
Object[] temp = new Object[len];
// 循环,把不包含在 c 里面的元素,放到新数组中
for (int i = 0; i < len; ++i) {
Object element = elements[i];
// 不包含在 c 中的元素,从 0 开始放到新数组中
if (!c.contains(element))
temp[newlen++] = element;
}
// 拷贝新数组,变相的删除了不包含在 c 中的元素
if (newlen != len) {
setArray(Arrays.copyOf(temp, newlen));
return true;
}
}
return false;
} finally {
lock.unlock();
}
}