简介
-
CopyOnWriteArrayList是ArrayList的线程安全版本,ArrayList可以参考【JDK源码】ArrayList,多线程环境下可以直接使用,无需加锁,内部也是通过数组实现,通过锁 + 数组拷贝 + volatile 关键字保证了线程安全,每次对数组的修改都完全拷贝一份新的数组来修改,修改完了再替换掉老数组,这样保证了只阻塞写操作,不阻塞读操作,实现读写分离。
-
CopyOnWriteArrayList 数据结构和 ArrayList 是一致的,底层是个数组,只不过 CopyOnWriteArrayList 在对数组进行操作的时候,基本会分四步走:
- 加锁;
- 从原数组中拷贝出新数组;
- 在新数组上进行操作,并把新数组赋值给数组容器;
- 解锁
除了加锁之外,CopyOnWriteArrayList 的底层数组还被 volatile
关键字修饰,意思是一旦数组被修改,其它线程立马能够感知到
继承体系
- CopyOnWriteArrayList实现了List, RandomAccess, Cloneable, java.io.Serializable等接口。
- CopyOnWriteArrayList实现了List,提供了基础的添加、删除、遍历等操作。
- CopyOnWriteArrayList实现了RandomAccess,提供了随机访问的能力。
- CopyOnWriteArrayList实现了Cloneable,可以被克隆。
- CopyOnWriteArrayList实现了Serializable,可以被序列化。
基本属性
/** 用于修改时加锁 */
final transient ReentrantLock lock = new ReentrantLock();
/** 真正存储元素的地方,只能通过getArray()/setArray()访问 volatile意思是一旦数组被修改,其它线程立马能够感知到*/
private transient volatile Object[] array;
- lock
用于修改时加锁,使用transient修饰表示不自动序列化。
- array
真正存储元素的地方,使用transient修饰表示不自动序列化,使用volatile修饰表示一个线程对这个字段的修改另外一个线程立即可见。
思考:为啥没有size字段?
构造方法
/**
* 创建一个空数组
*/
public CopyOnWriteArrayList() {
// 所有对array的操作都是通过setArray()和getArray()进行
setArray(new Object[0]);
}
/**
* 设置array
*/
final void setArray(Object[] a) {
array = a;
}
============================================
/**
* 以Collection集合为参数创建数组
*/
public CopyOnWriteArrayList(Collection<? extends E> c) {
Object[] elements;
//如果数组是CopyOnWriteArrayList类型,则将数组给到elements,注意这里是浅拷贝,两个集合共用一个数组
if (c.getClass() == CopyOnWriteArrayList.class)
elements = ((CopyOnWriteArrayList<?>)c).getArray();
else {
//否则调用toArray将集合转化位数组
elements = c.toArray();
// c.toArray might (incorrectly) not return Object[] (see 6260652)
//转化后的数组不一定是Object[]类型
//原因是:如果子类重写了toArray并将返回类型改为不是Object[]。
if (elements.getClass() != Object[].class)
//将elements以Object[]类型拷贝
elements = Arrays.copyOf(elements, elements.length, Object[].class);
}
setArray(elements);
}
============================================
/**
* 以数组为参数强转为Object[]类型创建数组
*/
public CopyOnWriteArrayList(E[] toCopyIn) {
//直接将toCopyIn数组以Object[]类型拷贝
setArray(Arrays.copyOf(toCopyIn, toCopyIn.length, Object[].class));
}
-
所有对
array
的操作都是通过setArray()和getArray()进行 -
创建的数组一定要满足
Object[]类型
add()
public boolean add(E e) {
//获取当前锁,也就是调用该方法的对象
final ReentrantLock lock = this.lock;
//加锁
lock.lock();
try {
// 将array数组给到elements临时数组
Object[] elements = getArray();
int len = elements.length;
// 先将数组进行len+1(想当于扩容一位)的拷贝,并且以原来类型拷贝
Object[] newElements = Arrays.copyOf(elements, len + 1);
// 在新数组中进行赋值,新元素直接放在数组的尾部
newElements[len] = e;
// 替换掉原来数组
setArray(newElements);
return true;
} finally {
// 释放锁
lock.unlock();
}
}
- 加锁==>获取元素数组==>新建一个数组,大小为原数组长度加1,并把原数组元素拷贝到新数组
- >把新数组赋值给当前对象的array属性,覆盖原数组>解锁
- 通过加锁,来保证同一时刻只能有一个线程能够对同一个数组进行 add 操作。
都已经加锁了,为什么需要拷贝数组,而不是在原来数组上面进行操作呢?
- volatile 关键字修饰的是数组,如果我们简单的在原来数组上修改其中某几个元素的值,是无法触发可见性的,我们必须通过修改数组的内存地址才行,也就说要对数组进行重新赋值才行。
- 在新的数组上进行拷贝,对老数组没有任何影响,只有新数组完全拷贝完成之后,外部才能访问到,降低了在赋值过程中,老数组数据变动的影响。
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;
// 如果给的index不在数组中,抛出异常
if (index > len || index < 0)
throw new IndexOutOfBoundsException("Index: "+index+
", Size: "+len);
Object[] newElements;
// 需要移动多少位元素 也就是index后面有多少元素
int numMoved = len - index;
// 等于0则插入最后一个
if (numMoved == 0)
// 直接拷贝并长度+1
newElements = Arrays.copyOf(elements, len + 1);
else {
newElements = new Object[len + 1];
// 拷贝旧数组前index的元素到新数组中
System.arraycopy(elements, 0, newElements, 0, index);
// 将index之后的元素往后挪一位拷贝到新数组中
System.arraycopy(elements, index, newElements, index + 1,
numMoved);
}
// 将元素放置在index处
newElements[index] = element;
setArray(newElements);
} finally {
lock.unlock();
}
}
- 添加位置不在数组索引中则抛出异常
- 添加位置在尾部就直接
一次拷贝
len+1添加 - 添加位置在中部,则将添加位置前后
两次拷贝
,然后将元素插入
小结
- 加锁:保证同一时刻数组只能被一个线程操作;
- 数组拷贝:保证数组的内存地址被修改,修改后触发 volatile 的可见性,其它线程可以立马知道数组已经被修改;
- volatile:值被修改后,其它线程能够立马感知最新值。
3 个要素缺一不可,比如说我们只使用 1 和 3 ,去掉 2,这样当我们修改数组中某个值时,并不会触发 volatile 的可见特性的,只有当数组内存地址被修改后,才能触发把最新值通知给其他线程的特性
get()
// 获取指定索引的元素,支持随机访问,时间复杂度为O(1)
public E get(int index) {
return get(getArray(), index);
}
@SuppressWarnings("unchecked")
private E get(Object[] a, int index) {
return (E) a[index];
}
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
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即可
- 删除中间一个元素,将删除元素位置的前后元素两次拷贝到新数组
size()
public int size() {
// 获取元素个数不需要加锁
// 直接返回数组的长度
return getArray().length;
}
为什么没有size属性呢?
因为每次修改都是拷贝一份正好可以存储目标个数元素的数组,所以不需要size属性了,数组的长度就是集合的大小,而不像ArrayList数组的长度实际是要大于集合的大小的。
比如,add(E e)操作,先拷贝一份n+1个元素的数组,再把新元素放到新数组的最后一位,这时新数组的长度为len+1了,也就是集合的size了。
总结
-
add、remove都要加锁,get不加锁。读写分离的思想,读操作不加锁,写操作加锁,且写操作占用较大内存空间,所以适用于
读多写少
的场合; -
写操作都要先拷贝
一份新数组,在新数组中做修改,修改完了再用新数组替换老数组,所以空间复杂度是O(n),性能比较低下
-
读操作支持随机访问,时间复杂度为O(1);
-
只保证最终一致性,不保证实时一致性