CopyOnWriteArrayList
聪明的小伙伴从文章的标题就知道今天我们要介绍的集合时CopyOnWriteArrayList,看名字就知道是ArrayList的衍生品,为了解决ArrayList线程不安全问题而创造出来的。
在正式进入CopyOnWriteArrayList之前让我们先看一下COW(Copy On Write)
Copy On Write
fork()和exec()
fork()
fork主要用于创建一个新进程,即子进程,创建出来的新进程是通过老进程复制而来。
linux系统中的init是所有进程的父亲,即Linux上的所有进程都是通过init进程或者其子进程fork出来的
exec()
需要注意的是exec()不是一个特定的函数,而是一组函数的统称,包括了execl()、execlp()、execv()、execle()、execve()、execvp()。
其主要的作用就是用于装载一个新的程序(可执行映射)覆盖当前进程内存空间中的映像,从而执行不同的任务。
其在执行的时候会直接替换掉当前进程的地址空间。
Linux下的COW
在传统做法下,在创建了子进程后,会将父进程的数据完全拷贝进子进程,拷贝完成后,父进程和子进程之间的数据段和堆栈是相互独立的。
但是由于往往子进程会执行exec()来实现属于自己的功能,这是复制过去的数据大多数情况是没用的。为了避免无效的复制,COW技术就出现了。
COW:
- fork创建出的子进程,与父进程共享内存空间,如果父子进程不对内存空间进行写入操作的话,内存空间中的数据并不会复制给子进程。使得创建速度加快
- 并且如果在fork函数返回以后,子进程第一时间exec一个新的可执行映射,那么也就不会浪费时间和内存空间。
COW技术的好处:
- 减少分配和复制资源带来的时间消耗和资源分配
COW技术的缺点:
- 如果在fork()以后,父子进程继续进程写操作,那么会产生大量的分页错误,这样得不偿失
CopyOnWriteArrayList
通过上面的介绍我们知道了cow技术的优点以及缺点,但是要想保证ArrayList的安全性,不止有CopyOnWriteArrayList容器还有另外的两种方法,分别是:Vector和SynchronizedList。但是其都有各自的问题。
Vector和SynchronizedList问题
其中第一个问题在于,其中的方法并不是原子性的,即使他们内部的每一步的操作都是原子性的(被Synchronized修饰就可以实现原子性),但是其内部之间还可以交替进行。
出现这种问题的原因是因为我们都是对Vector进行操作的,所以对应的解决方法就是在操作Vector前锁住即可。
但是就算是锁住了,但是在遍历Vector的时候,有别的线程修改了Vector的长度,也是会有问题的。
在JDK5以后java推荐使用for-each(迭代器)来遍历集合,好处是简洁、数组索引的边界值只计算一次。
但是对于此问题就算使用迭代器也是会抛出异常。
要想真正解决这些问题,就需要我们在遍历前加锁,但是会导致效率低下。所以这个时候就需要我们的CopyOnWriteArrayList来解决问题。
CopyOnWriteArrayList介绍
一般来说我们认为CopyOnWriteArrayList(Set)是同步List(Set)的代替品。
-
CopyOnWrite和Vector(HashTable)比较
-
- Vector和Hashtable最大的问题在于加锁的粒度大(直接在方法中使用Synchronized)
- 而CopyOnWriteArrayList加锁粒度小(用各种的方式来实现线程安全(cas锁、volatile))
- JUC下的线程安全容器在遍历的时候不会抛出ConcurrentModificationException异常
实现原理
- 总览:
其是线程安全容器(相对于ArrayList),底层通过复制数组的方式来实现。
其在遍历的时候不用额外加锁,且不会抛出ConcurrentModificationException异常。
元素可以为null
基本结构
/** 可重入锁对象 */
final transient ReentrantLock lock = new ReentrantLock();
/** CopyOnWriteArrayList底层由数组实现,volatile修饰 */
private transient volatile Object[] array;
/**
* 得到数组
*/
final Object[] getArray() {
return array;
}
/**
* 设置数组
*/
final void setArray(Object[] a) {
array = a;
}
/**
* 初始化CopyOnWriteArrayList相当于初始化数组
*/
public CopyOnWriteArrayList() {
setArray(new Object[0]);
}
从基本结构来看,其底层就是数组,加锁就由ReentrantLock来实现。
常见方法
- add()
首先我们来看一下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 {
// 最后在finally中释放锁
lock.unlock();
}
}
从源码阅读的过程中我们可以很清晰的看到,其核心思想就是复制出一个新数组然后在新数组上进行添加操作。然后将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;
Object[] newElements = Arrays.copyOf(elements, len);
newElements[index] = element;
// 改变指向
setArray(newElements);
} else {
// Not quite a no-op; ensures volatile write semantics
setArray(elements);
}
return oldValue;
} finally {
lock.unlock();
}
}
set方法和add方法的思路大体一致
-
总结
-
- 在修改时,会首先复制一个新数组,然后在新数组中进行修改,然后将array指向新数组
- 写加锁,读不加锁
为什么遍历时不用调用者显示加锁
在上面减少和ArrayList的对比中我们提到,其有一点就是不会抛出异常,想要了解这个问题的答案还是要通过代码来进行分析
// 1. 返回的迭代器是COWIterator
public Iterator<E> iterator() {
return new COWIterator<E>(getArray(), 0);
}
// 2. 迭代器的成员属性
private final Object[] snapshot;
private int cursor;
// 3. 迭代器的构造方法
private COWIterator(Object[] elements, int initialCursor) {
cursor = initialCursor;
snapshot = elements;
}
// 4. 迭代器的方法...
public E next() {
if (! hasNext())
throw new NoSuchElementException();
return (E) snapshot[cursor++];
}
//.... 可以发现的是,迭代器所有的操作都基于snapshot数组,而snapshot是传递进来的array数组
通过代码我们可以很清楚的发现,其在使用迭代器遍历的时候,操作的数组都是原数组。
缺点
通过前面的分析我们可以知道其也存在一些严重的缺点或者问题:
- 内存占用:因为在修改的时候,需要赋值一个新数组导致内存占用很大
- 数据一致性问题:通过迭代的代码我们可以知道其只保证数据的最终一致性,并不保证数据的实时一致性。
最后
- 如果觉得看完有收获,希望能给我点个赞,这将会是我更新的最大动力,感谢各位的支持
- 欢迎各位关注我的公众号【java冢狐】,专注于java和计算机基础知识,保证让你看完有所收获,不信你打我
- 如果看完有不同的意见或者建议,欢迎多多评论一起交流。感谢各位的支持以及厚爱。