1. 同步容器和并发容器
Java1.5以前,提供线程安全的容器是同步容器,比如Vector、Stack 和 Hashtable,这些容器使用synchronized关键字修饰方法,以保证互斥。这种方式的串行度太高,所以效率很低。
Java1.5开始,提供了性能更好的并发容器。并发容器也可分成四大类:List、Map、Set和Queue。
2. CopyOnWriteArrayList
CopyOnWriteArrayList是ArrayList的线程安全版本,从名称上来看,它是写时复制的ArrayList。在java1.5,由并发大师Doug Lea所设计。该类可以存储元素null。
2.1 继承关系
CopyOnWriteArrayList并没有继承ArrayList,也没有继承AbstractList,而只是实现了List接口,因为它的实现方式与ArrayList不太相同。
public class CopyOnWriteArrayList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable
2.2 成员属性
(1)一把可重入的锁,用于保证写操作时的线程同步。因为锁不可被修改,所以使用final关键字进行修饰。
final transient ReentrantLock lock = new ReentrantLock();
(2)Object数组用于存放容器中的元素,使用volatile关键字修饰,禁用缓存,防止出现线程安全问题。
private transient volatile Object[] array;
2.3 构造器
(1)空参构造器,仅创造了一个长度为0的Object数组。
public CopyOnWriteArrayList() {
setArray(new Object[0]);
}
final void setArray(Object[] a) {
array = a;
}
(2)将容器c中的所有元素都添加到新容器的构造器,添加顺序按照容器c的迭代顺序。
public CopyOnWriteArrayList(Collection<? extends E> c) {
Object[] elements;
//如果容器c的类型正好是CopyOnWriteArrayList,则可以直接取其内部的数组,作为新容器的内部数组
if (c.getClass() == CopyOnWriteArrayList.class)
elements = ((CopyOnWriteArrayList<?>)c).getArray();
else {
//否则,调用该容器类自身的toArray方法,该方法通常会复制一份容器内部的数组,然后返回副本
elements = c.toArray();
//如果c的类型不是ArrayList,那么其持有的数组可能不是Object类型的,
//需要将该数组中的元素全部复制到一个新的Object数组,再引用新数组。
if (c.getClass() != java.util.ArrayList.class)
elements = Arrays.copyOf(elements, elements.length, Object[].class);
}
setArray(elements);
}
(3)构造器传入一个E类型的数组,E是CopyOnWriteArrayList类的泛型参数,将该数组元素全部复制到一个新建的Object数组中,然后容器内部引用这个新数组。
public CopyOnWriteArrayList(E[] toCopyIn) {
setArray(Arrays.copyOf(toCopyIn, toCopyIn.length, Object[].class));
}
2.4 添加元素
(1)add(E)方法添加一个元素到容器中。
对于容器的写操作,CopyOnWriteArrayList执行写时复制策略。首先会把原数组的内容全部复制到新数组中,再把要添加到元素写入新数组。最后,让本容器引用新数组。
写操作是加锁的,保证了同一时刻只有一个线程执行写操作,防止多个线程同时创建出多个副本。
public boolean add(E e) {
//设置一把锁,这把锁是本类中的成员对象lock
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执行写操作的成本非常高,每个写操作不仅要互斥执行,还要创建一个副本。
(2)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)
throw new IndexOutOfBoundsException("Index: "+index+
", Size: "+len);
Object[] newElements;
//要移动的元素数
int numMoved = len - index;
//如果是在原数组尾部添加,则不需要移动任何元素,只需要创建一个长度加1的数组
if (numMoved == 0)
newElements = Arrays.copyOf(elements, len + 1);
else {
//创建一个长度加1的数组
newElements = new Object[len + 1];
//拷贝0到index-1的元素到新数组中
System.arraycopy(elements, 0, newElements, 0, index);
//拷贝index及后面的元素到新数组中,从新数组的index+1的位置开始放
System.arraycopy(elements, index, newElements, index + 1,
numMoved);
}
//将新元素放入新数组的index位置,再引用新数组
newElements[index] = element;
setArray(newElements);
} finally {
lock.unlock();
}
}
2.5 查找元素
CopyOnWriteArrayList的读操作不使用互斥锁,因为读操作不会改变容器的内容。
(1)contains(Object o)方法判断容器中是否包含某个元素
public boolean contains(Object o) {
Object[] elements = getArray();
return indexOf(o, elements, 0, elements.length) >= 0;
}
indexOf方法从参数的index方法开始查找,当到达参数fence位置时,停止查找,查找范围不包括fence位置。找到则会返回该元素的索引,找不到会返回-1。
private static int indexOf(Object o, Object[] elements,
int index, int fence) {
//因为容器中可以存放null,而null可能会引发空指针异常,所以要单独判断
if (o == null) {
for (int i = index; i < fence; i++)
if (elements[i] == null)
return i;
} else {
for (int i = index; i < fence; i++)
if (o.equals(elements[i]))
return i;
}
return -1;
}
(2)indexOf(Object o)方法查找某个元素的第一个索引位置。
public int indexOf(Object o) {
Object[] elements = getArray();
return indexOf(o, elements, 0, elements.length);
}
(3)lastIndexOf(Object o)方法查找某个元素的最后一个索引位置。其实就是从后往前查找第一个相同的元素。
public int lastIndexOf(Object o) {
Object[] elements = getArray();
return lastIndexOf(o, elements, elements.length - 1);
}
(4)get(int index)方法获取某个索引位置的元素。
public E get(int index) {
return get(getArray(), index);
}
2.6 删除元素
(1)remove(int index)方法删除给定位置上的元素。
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 {
//否则,需要将index后面的元素前移一位
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();
}
}
(2)remove(Object o)方法删除给定元素第一次出现的位置上的元素。
public boolean remove(Object o) {
//取得当前时刻容器内的数组,作为快照snapshot
Object[] snapshot = getArray();
//获取该元素在快照中的位置
int index = indexOf(o, snapshot, 0, snapshot.length);
//快照中没有,就不用删
return (index < 0) ? false : remove(o, snapshot, index);
}
真正执行删除操作时,要加锁。
由于在取得元素在快照中的位置开始,到真正删除元素前,由于没有加锁,这期间容器可能发生写操作。
如果发生了写操作,内部持有的数组会改变,不同于之前拍下的快照,此时要重新查找要删除的元素位置。
private boolean remove(Object o, Object[] snapshot, int index) {
final ReentrantLock lock = this.lock;
lock.lock();
try {
Object[] current = getArray();
int len = current.length;
//如果在拍下快照后,删除元素前,期间,容器发生了写操作,则要重新查找要删除的元素的位置
if (snapshot != current) findIndex: {
//由于容器经过修改后,数组长度可能会小于之前的index,所以要先取得两者的最小值
int prefix = Math.min(index, len);
//找index或者len前面是否有元素被改为了要删除的元素,如果找到,那个位置的元素就是现在要删除的
for (int i = 0; i < prefix; i++) {
//为什么做以下判断?原因如下:
//如果当前位置的指针没有发生过变化,则肯定不是要删除的,可以直接跳过equals验证
//只有当前位置的指针发生了变化,才可能存在当前位置变成了要删除的元素,然后再进一步验证是否真的要删除
if (current[i] != snapshot[i] && eq(o, current[i])) {
index = i;
break findIndex;
}
}
//如果index依然超出len,则没有要删除的元素
if (index >= len)
return false;
//如果index<len,并且找到了,则可以退出代码块
if (current[index] == o)
break findIndex;
//如果没找到,就要从index开始往后找
index = indexOf(o, current, index, len);
//还是没找到,返回false
if (index < 0)
return false;
}
//找到后,执行删除操作,同样是要将数组复制,在新数组中执行删除
Object[] newElements = new Object[len - 1];
System.arraycopy(current, 0, newElements, 0, index);
System.arraycopy(current, index + 1,
newElements, index,
len - index - 1);
setArray(newElements);
return true;
} finally {
lock.unlock();
}
}
(3)removeAll(Collection<?> 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;
if (len != 0) {
// 创建一个临时数组,保存不用删除的元素
int newlen = 0;
Object[] temp = new Object[len];
for (int i = 0; i < len; ++i) {
Object element = elements[i];
//如果当前元素不是c中的元素,则保留
if (!c.contains(element))
temp[newlen++] = element;
}
//如果存在要删除的元素,则将临时数组temp拷贝一份,本容器的数组引用该副本
if (newlen != len) {
setArray(Arrays.copyOf(temp, newlen));
return true;
}
}
return false;
} finally {
lock.unlock();
}
}
2.7 修改元素
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 {
// 否则,不作修改
setArray(elements);
}
return oldValue;
} finally {
lock.unlock();
}
}
2.8 迭代器
iterator() 方法获取COWIterator迭代器,将当前时刻的数组和起始遍历位置作为参数传入。
public Iterator<E> iterator() {
return new COWIterator<E>(getArray(), 0);
}
COWIterator类存储了获取迭代器实例时,容器中持有的数组快照,以及下一个要返回的元素位置。
static final class COWIterator<E> implements ListIterator<E> {
/** Snapshot of the array */
private final Object[] snapshot;
/** Index of element to be returned by subsequent call to next. */
private int cursor;
构造器初始化快照snapshot和下一个要返回的元素位置cursor。迭代器中所有的操作都是在快照上进行的。
private COWIterator(Object[] elements, int initialCursor) {
cursor = initialCursor;
snapshot = elements;
}
hasNext() 方法。cursor小于数组快照的长度时,存在下一个要返回的元素。
public boolean hasNext() {
return cursor < snapshot.length;
}
hasPrevious()方法。cursor大于0时,存在前项元素。这里的前项元素就是指next-1位置上的元素。
public boolean hasPrevious() {
return cursor > 0;
}
next()方法获取下一个位置的元素。
public E next() {
if (! hasNext())
throw new NoSuchElementException();
//游标后移一位
return (E) snapshot[cursor++];
}
注意:CopyOnWriteArrayList的迭代器不支持写操作。可以看到如果执行写操作,会直接抛异常。
public void remove() {
throw new UnsupportedOperationException();
}
public void set(E e) {
throw new UnsupportedOperationException();
}
public void add(E e) {
throw new UnsupportedOperationException();
}
由于迭代器的所有操作都是在快照上进行的,写操作在副本上执行,不影响快照的内容。
因此,CopyOnWriteArrayList不会触发fast-fail事件,其他的同步容器也是如此。
同时,由于迭代器遍历的旧版本的数组,不会及时读到新的改变。
2.9 小结
①CopyOnWriteArrayList对读操作不加锁,写操作会加锁。
执行写操作时,不在原数组上进行操作,而是创建一个数组副本,在副本上执行写操作。执行完毕后,引用新数组。
因此,CopyOnWriteArrayList适用于读多写少的场景,因为它执行写操作的开销太大。
②迭代器只做读操作,并且是在迭代器实例创建时所获取的数组快照上进行遍历。写操作不会影响这个快照,所以CopyOnWriteArrayList不会触发fast-fail机制。
由于迭代器是在旧快照上遍历,不能及时读到容器的新变化。因此,CopyOnWriteArrayList适用于能够容忍读写短暂不一致的场景。