简介
CopyOnWriteArrayList是jdk concurrent包中提供的一个非阻塞型的,线程安全的List实现。CopyOnWriteArrayList是一个线程安全、并且在读操作时无锁的ArrayList。
对CopyOnWriteArrayList进行读取时,不进行数据锁定,直接返回需要查询的数据,如果需要返回整个数组,那么会将整个数组拷贝一份,再返回,保证内部array在任何情况下都是只读的。
“CopyOnWrite”简介
在学习Linux操作系统进程/线程的时候就听到过这个概念,直接翻译过来叫“写时拷贝”,说的是当需要创建一个新线程,做fork()调用,而这时并未真正意义上开辟一块新的线程内存资源,仅当线程有“写操作”时,才会拷贝父线程的数据作为新线程的初始内容。
而在JavaSE5的并发包中,CopyOnWriteArrayList的实现也是利用了类似的方法。当数组内容有所变化时,拷贝一份新的出来,并把对象引用赋值给List的属性,旧数组的访问也依然有效,不会受到新的操作干扰。
CopyOnWriteArrayList适合什么场景?
容器对象的改动有很多种,对于CopyOnWriteArrayList每发生一次改变,CopyOnWriteArrayList就会复制一份数据,显然这个也是需要成本的。所以,CopyOnWriteArrayList其实是更适用于读操作远远多于写操作的场景。
CopyOnWriteArrayList
public class CopyOnWriteArrayList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable
但即使有了CopyOnWrite,在写操作时,创建新数组对象,并将其引用赋值时,仍然是会受到多线程的影响,这个还是要通过锁来保护。
transient
final
ReentrantLock lock =
new
ReentrantLock();
这就是CopyOnWriteArrayList类中的锁属性。
CopyOnWriteArrayList中,包含一个array数组对象,这个对象,只能由getArray()和setArray()两个方法访问,源码如下:/** The array, accessed only via getArray/setArray. */
private volatile transient Object[] array;
/**
* Gets the array. Non-private so as to also be accessible
* from CopyOnWriteArraySet class.
*/
final Object[] getArray() {
return array;
}
/**
* Sets the array.
*/
final void setArray(Object[] a) {
array = a;
}
CopyOnWriteArrayList构造函数
- CopyOnWriteArrayList():构造一个内容为空的对象
- CopyOnWriteArrayList(Collection<? extends E> c):根据传入参数中的内容,构造一个新的对象
- CopyOnWriteArrayList(E[] toCopyIn):根据传入数组中的内容,构造一个新的对象
1 public CopyOnWriteArrayList() {
setArray(new Object[0]);
}
创建一个大小为0的数组
2 public CopyOnWriteArrayList(Collection<? extends E> c) {
Object[] elements = c.toArray();
// c.toArray might (incorrectly) not return Object[] (see 6260652)
if (elements.getClass() != Object[].class)
elements = Arrays.copyOf(elements, elements.length, Object[].class);
setArray(elements);
}
将传入的容器转换为数组,如果传入的元素不能直接转换为数组类型,创建一个数组然后将复制到新数组,然后通过一个原子操作设置给array属性。
根据传入参数c的长度,构造一个同样长度的Object[]对象,并且将c的内容,依次填入此Object[]对象中
(1). 这里的构造函数,未显式判断c是否为null,实际上如果c为null,会抛出空指针异常
(2). 这里对于c中内容的复制,是浅拷贝而非深拷贝
深拷贝(深复制)和浅拷贝(浅复制)概念解释:
深拷贝(深复制)和浅拷贝(浅复制)是两个比较通用的概念,尤其在C++语言中,若不弄懂,则会在delete的时候出问题,但是我们在这幸好用的是Java。虽然java自动管理对象的回收,但对于深拷贝(深复制)和浅拷贝(浅复制),我们还是要给予足够的重视,因为有时这两个概念往往会给我们带来不小的困惑。
浅拷贝是指拷贝对象时仅仅拷贝对象本身(包括对象中的基本变量),而不拷贝对象包含的引用指向的对象。深拷贝不仅拷贝对象本身,而且拷贝对象包含的引用指向的所有对象。举例来说更加清楚:对象A1中包含对B1的引用,B1中包含对C1的引用。浅拷贝A1得到A2,A2 中依然包含对B1的引用,B1中依然包含对C1的引用。深拷贝则是对浅拷贝的递归,深拷贝A1得到A2,A2中包含对B2(B1的copy)的引用,B2 中包含对C2(C1的copy)的引用。
若不对clone()方法进行改写,则调用此方法得到的对象即为浅拷贝3 public CopyOnWriteArrayList(E[] toCopyIn) {
setArray(Arrays.copyOf(toCopyIn, toCopyIn.length, Object[].class));
}
根据传入参数的长度,构造出一个同样长度,内容一致的数组对象,封装在CopyOnWriteArrayList中
CopyOnWriteArrayList读操作
读的相关操作
对于读操作来讲,是不需要加锁的,直接读取引用对应的对象即可。因为即使数组有所改动,也是开辟新的数组并在新的数组中做修改,旧数组对象是始终可用的。
public E get(int index): 根据索引获取对应的元素,读取不需要加锁
public Iterator<E> iterator() 调用iterator方法后创建一个新的COWIterator对象实例,并保存了一个当前数组的快照,在调用next遍历时则仅仅对快照数组进行遍历,因此遍历CopyOnWriteArrayList时不会抛出ConcurrentModificatiedException。
public ListIterator<E> listIterator()
public ListIterator<E> listIterator(final int index)
public E get(int index) {
return (E)(getArray()[index]);
}
该操作简单,直接获取当前数组对应位置的元素,这种方法是没有加锁保护的,因此可能会出现读到脏数据的现象。但相对而言,性能会非常高,对于写少读多且脏数据影响不大的场景而言,CopyOnWriteArrayList是不错的选择。
2 iterator()
public Iterator<E> iterator() {
return new COWIterator<E>(getArray(), 0);
}
调用iterator方法后创建一个新的COWIterator对象实例,并保存了一个当前数组的快照,在调用next遍历时则仅仅对快照数组进行遍历,因此遍历CopyOnWriteArrayList时不会抛出ConcurrentModificatiedException。
迭代器Iterator的使用。CopyOnWriteArrayList内部给出了COWIterator类的实现,也非常简单,只有数组snapshot属性和索引cursor属性。snapshot的名字定义得非常好,意为“快照”。实际上CopyOnWriteArrayList的迭代器对象就是在生成时的那一个快照,后续的迭代读操作都是在这个快照基础上进行的。
COWIterator意思CopyOnWriteIterator
private static 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;
private COWIterator(Object[] elements, int initialCursor) {
cursor = initialCursor;
snapshot = elements;
}
public boolean hasNext() {
return cursor < snapshot.length;
}
public boolean hasPrevious() {
return cursor > 0;
}
public E next() {
if (! hasNext())
throw new NoSuchElementException();
return (E) snapshot[cursor++];
}
public E previous() {
if (! hasPrevious())
throw new NoSuchElementException();
return (E) snapshot[--cursor];
}
public int nextIndex() {
return cursor;
}
public int previousIndex() {
return cursor-1;
}
/**
* Not supported. Always throws UnsupportedOperationException.
* @throws UnsupportedOperationException always; <tt>remove</tt>
* is not supported by this iterator.
*/
public void remove() {
throw new UnsupportedOperationException();
}
/**
* Not supported. Always throws UnsupportedOperationException.
* @throws UnsupportedOperationException always; <tt>set</tt>
* is not supported by this iterator.
*/
public void set(E e) {
throw new UnsupportedOperationException();
}
/**
* Not supported. Always throws UnsupportedOperationException.
* @throws UnsupportedOperationException always; <tt>add</tt>
* is not supported by this iterator.
*/
public void add(E e) {
throw new UnsupportedOperationException();
}
}
CopyOnWriteArrayList写操作
CopyOnWriteArrayList中是有ReentrantLock类的锁对象属性的。在修改CopyOnWriteArrayList类的对象时,是需要考虑多个线程访问array对象的。那么,锁的使用也就是必须的了。在CopyOnWriteArrayList中,CopyOnWrite的过程通常是这样进行的。先在原数组对象属性的基础上copy出一个新的数组,其它改动操作都在新的数组对象上完成,当所有修改操作完成后,将新数组对象的引用交给CopyOnWriteArrayList类的array属性对象。整个过程在锁的保护下进行。
1 新增
public boolean add(E e) 加锁操作,将元素e写入列表尾部
public void add(int index, E element) 将元素插入到索引位置index
public boolean addAll(int index, Collection<? extends E> c) 将集合c插入到索引index位置
public boolean addAll(Collection<? extends E> c) 将集合c插入到列表尾部
public boolean addIfAbsent(E e) 如果元素e不在列表中则将元素插入到列表中
public int addAllAbsent(Collection<? extends E> c) 如果集合c不在列表中,则将集合c插入到列表中
2 修改
public E set(int index, E element) 用元素element替换index位置的元素
3 删除
public E remove(int index) 删除index位置的元素
public boolean remove(Object o) 删除元素o
public boolean removeAll(Collection<?> c) 删除集合c
1.1 add(E e)
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();
}
}
分析:add方法没有加关键字synchronized,而是通过ReentrantLock来保证线程安全,此处和ArrayList的不同是每次都会创建一个新的Object数组,此数组的大小为当前数组大小加1,将之前数组中的内容复制到新的数组中,并将新增加的对象放入数组末尾,最后做引用切换将新创建的数组对象赋值给全局的数组对象。
public boolean remove(Object o) {
final ReentrantLock lock = this.lock;
lock.lock();
try {
Object[] elements = getArray();
int len = elements.length;
if (len != 0) {
// Copy while searching for element to remove
// This wins in the normal case of element being present
int newlen = len - 1;
Object[] newElements = new Object[newlen];
for (int i = 0; i < newlen; ++i) {
if (eq(o, elements[i])) {
// found one; copy remaining and exit
for (int k = i + 1; k < len; ++k)
newElements[k-1] = elements[k];
setArray(newElements);
return true;
} else
newElements[i] = elements[i];
}
// special handling for last cell
if (eq(o, elements[newlen])) {
setArray(newElements);
return true;
}
}
return false;
} finally {
lock.unlock();
}
}
分析:该方法也是通过ReentrantLock来保证线程安全的,首先创建一个比当前数组小1的数组,遍历数组,如找到equals或均为null的元素,则将之后的元素全部赋值给新的数组,并做引用切换,返回true,如未找到,则将当前的元素赋值给新的数组对象,最后特殊处理数组中的最后一个元素,如最后一个元素等于要删除的元素,即将当前数组对象赋值为新创建的数组对象,完成删除动作,如最后一个元素也不等于要删除的元素,那么返回false. 此方法复制过程没有调用System的arrayCopy来完成,
CopyOnWriteArrayList其他操作
public boolean isEmpty()public boolean contains(Object o)
public boolean containsAll(Collection<?> c)
public int size()
public boolean equals(Object o)
public void clear() 清除数组
CopyOnWriteArraySet简介
在java.util.concurrent包中,除了CopyOnWriteArrayList类,也还有这样一个类CopyOnWriteArraySet。CopyOnWriteArraySet的实现是完全基于CopyOnWriteArrayList的。public class CopyOnWriteArraySet<E> extends AbstractSet<E> implements java.io.Serializable
构造函数: private final CopyOnWriteArrayList<E> al;
/**
* Creates an empty set.
*/
public CopyOnWriteArraySet() {
al = new CopyOnWriteArrayList<E>();
}
可以看出CopyOnWriteArraySet基于CopyOnWriteArrayList实现,其唯一的不同是在add时调用的是CopyOnWriteArrayList的addIfAbsent方法。addIfAbsent方法同样采用锁保护,并且在一个新的大小+1的Object数组。遍历当前Object[]数组,如Object[]数组中已有了当前元素,则直接返回,如没有则放入Object[]数组的尾部,并返回。从分析可以看出CopyOnWriteArraySet在add时每次都要进行数组的遍历,因此其性能会略低于CopyOnWriteArrayList。
更详细的大家可以参看JDK,欢迎补充。