这JUC包里最典型的解决ArrayList线程不安全问题的数据结构,采用读写分离的思想,比Vector和Collections.synchronizedList高效。
目录
成员变量
public class CopyOnWriteArrayList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable {
private static final long serialVersionUID = 8673264195747942595L;
//保护所有突变器的锁
final transient ReentrantLock lock = new ReentrantLock();
/** 数组,只能通过 getArray/setArray 访问。 */
private transient volatile Object[] array;
}
这里说明底层内存数据结构和ArrayList一样,为array数组。值得注意的是,这里用了JUC包可重锁ReetrantLock,这和Synchronized关键字还是有些异同点的,它们的底层实现不一样,适用场景也不一样,后面我将出一篇专门的文章来讨论,这里只需要直到其是”锁“,用于同步即可了。volatile关键字也是为了实现多线程环境下:数组的可见性、有序性(防止内存重排)。
构造函数
/**
创建一个空列表。
*/
public CopyOnWriteArrayList() {
setArray(new Object[0]); //空
}
/**
创建一个包含指定集合的元素的列表,按照集合的迭代器返回的顺序。
参数:
c – 最初持有的元素的集合
抛出:
NullPointerException – 如果指定的集合为空。
*/
public CopyOnWriteArrayList(Collection<? extends E> c) {
Object[] elements;
if (c.getClass() == CopyOnWriteArrayList.class)
elements = ((CopyOnWriteArrayList<?>)c).getArray();
else {
elements = c.toArray(); //转数组
if (c.getClass() != java.util.ArrayList.class)
elements = Arrays.copyOf(elements, elements.length, Object[].class);
}
setArray(elements);
}
/**
创建一个包含给定数组副本的列表。
参数:
toCopyIn – 数组(此数组的副本用作内部数组)
抛出:
NullPointerException – 如果指定的数组为空
*/
public CopyOnWriteArrayList(E[] toCopyIn) {
setArray(Arrays.copyOf(toCopyIn, toCopyIn.length, Object[].class));
}
/**
*设置数组。
*/
final void setArray(Object[] a) {
array = a;
}
构造函数即底层array的赋值操作。
添加
/**
将指定的元素附加到此列表的末尾。
参数:
e - 要附加到此列表的元素
返回:
true (由Collection.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();//解锁
}
}
这里可以看出由于锁的原因,多线程时不能同时对同一个CopyOnWriteArrayList对象进行add。至于它的高效性,在于读写分离,这要结合下面的查询操作来说明。
更改操作
/**
用指定的元素替换此列表中指定位置的元素。
抛出:
IndexOutOfBoundsException –
*/
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(); //解锁
}
}
这里有个重点:当数据并未修改时进行了 setArray(elements);操作。
为什么要出现这个呢?因为毕竟数组内容并未发生改变——这里要从volatile的可见性和有序性说起。即我们希望发生这样的事:线程a对volatile变量进行了写入,之后线程b对volatile变量进行了读操作,我们希望线程b能够发现线程a的写操作,即这时候b读取的内容要正确并且直到时间顺序是a->b。
假设a,b还有其它的操作:
int a=0;
new Thread(()->{
a=1; //1
cow.set(...)//2
},"a").start();
new Thread(()->{
cow.get();//3
int b=a;//4
},"b").start();
即使set方法中数据未发生变化,我们也需要感知到语句2在3之前执行,这样才能保证1在4之前执行。所以 setArray(elements)正是体现了volatile可见性、有序性的作用。
查询
/**
返回此列表中指定位置的元素。
抛出:
IndexOutOfBoundsException –
*/
public E get(int index) {
return get(getArray(), index);
}
/**
获取数组。 非私有以便也可以从 CopyOnWriteArraySet 类访问。
*/
final Object[] getArray() {
return array;
}
@SuppressWarnings("unchecked")
private E get(Object[] a, int index) {
return (E) a[index];
}
从这就可以看出,查询操作就是在老数组 array上直接查询的,并且不用加锁。也就是说如果目前添加操作和修改操作正在执行的话,两者相互不干扰,只是现在读取的是添加/修改操作之前的数据。
删除
/**
移除此列表中指定位置的元素。 将任何后续元素向左移动(从它们的索引中减去一个)。 返回从列表中删除的元素。
抛出:
IndexOutOfBoundsException –
*/
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];
System.arraycopy(elements, 0, newElements, 0, index);
System.arraycopy(elements, index + 1, newElements, index,
numMoved);
setArray(newElements); //赋值
}
return oldValue;
} finally {
lock.unlock();//解锁
}
}
删除操作也类似,分为删除最后一个元素(一次arraycopy)和中间元素(两次arraycopy)。