CopyOnWriteArrayList
一,类基本属性
public class CopyOnWriteArrayList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable {
//可重入锁
final transient ReentrantLock lock = new ReentrantLock();
//底层是个volatile修饰的array
private transient volatile Object[] array;
final Object[] getArray() {
return array;
}
final void setArray(Object[] a) {
array = a;
}
public CopyOnWriteArrayList() {
setArray(new Object[0]);
}
可见初始化方法是将new Object[0]赋值给了array。
二,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();
}
}
可见是使用了重入锁加索,保证的线程安全。
使用Arrays.copyOf,复制原有数据到新的数据中,开销较大。
三,读数据
public E get(int index) {
return get(getArray(), index);
}
private E get(Object[] a, int index) {
return (E) a[index];
}
直接取对应下标中的数据,并没有加锁。
四,读没有加锁,是怎么保证可见性的呢?
private transient volatile Object[] array;
由于array被volatile修饰,所以每次读取都会从主存中读取,故以volatile关键字,保证的不加锁实现的可见性。
五,A线程在循环读取时,B线程删除数据,会导致下标越界吗?
public Iterator<E> iterator() {
return new COWIterator<E>(getArray(), 0);
}
//迭代器的实现
static final class COWIterator<E> implements ListIterator<E> {
private final Object[] snapshot;
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++];
}
通过分析,我们发现A线程读取的是快照即原始数据,B线程修改不会影响A线程的遍历内容和结果。故不会报错。
六.volatile只修饰了array的引用,修改数据怎么保证数组内容的可见性?
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;
//可以看到新建的array
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();
}
}
是的,如果仅仅是array引用被volatile修饰的话,是无法保证其数组内容的可见性的,所以这里的实现是修改时重新copy数据,从而使array引用发现改变。保证了修改数组数据的可见性。修改一个数据要复制整个数组,代价也相当的高了。
综上,CopyOnWriteArrayList 通过重入锁加在写操作上,并且对底层数组使用volatile修饰,从而实现的线程安全和读不加锁。由于增加和修改都需要复制整个数组,故适合读多写少的场景。
CopyOnWriteArraySet
一,构造函数
public class CopyOnWriteArraySet<E> extends AbstractSet<E>
implements java.io.Serializable {
private static final long serialVersionUID = 5457747651344034263L;
private final CopyOnWriteArrayList<E> al;
//内部是一个CopyOnWriteArrayList
public CopyOnWriteArraySet() {
al = new CopyOnWriteArrayList<E>();
}
惊奇的发现,CopyOnWriteArraySet的内部竟然是个CopyOnWriteArrayList。
二,那么怎么实现的set唯一性的呢?
public boolean add(E e) {
return al.addIfAbsent(e);
}
public boolean addIfAbsent(E e) {
//注意,此时并未加锁
Object[] snapshot = getArray();
//如果快照中有,则直接返回false,这样可以减少锁的开销。
return indexOf(e, snapshot, 0, snapshot.length) >= 0 ? false :
addIfAbsent(e, snapshot);
}
private boolean addIfAbsent(E e, Object[] snapshot) {
final ReentrantLock lock = this.lock;
lock.lock();
try {
//重新获取底层array
Object[] current = getArray();
int len = current.length;
//如果array发生了改变,需再次判断是否已存在
if (snapshot != current) {
// Optimize for lost race to another addXXX operation
int common = Math.min(snapshot.length, len);
for (int i = 0; i < common; i++)
if (current[i] != snapshot[i] && eq(e, current[i]))
return false;
if (indexOf(e, current, common, len) >= 0)
return false;
}
Object[] newElements = Arrays.copyOf(current, len + 1);
newElements[len] = e;
setArray(newElements);
return true;
} finally {
lock.unlock();
}
}
靠的是遍历查询,如果没有再add。其中add的过程较CopyOnWriteArrayList复杂,主要是想避免比较是否存在时产生的锁的开销。
另外遍历查询的效率也太低了,时间复杂度是O(n),远远不如hashmap的O(1)。所以这个set在大数据量的情况下,性能堪忧。