一、把我能想到的写下来:
并发包中只有一个 List:CopyOnWriteArrayList,写时复制。对于数组的 增删改,都是通过先获取锁 ReetrantLock ,通过 ReetrantLock 的lock() 获取锁,保证只有一个线程对数组进行修改,然后 unlock() 方法将锁释放,该类中有个 object 类型的数组 array 。
(补:
CopyOnWriteArrayList 是个线程安全的 ArraytList,对其进行的操作 都是在底层的一个复制的数组 (快照:关于指定数据集合的一个完全可用拷贝 )。
)
初始化:如果是无参构造,就会创建一个 长度为0 的array 。
add() 方法:先获取独占锁,然后将原数组利用 Aaarys.copyOf() 进行拷贝,使引用 newArray 指向拷贝的数组,并在 newArray 中添加元素,然后还会用一次 Array.sort() ,拷贝新数组,将 array 引用指向拷贝后的新数组。
(改:
只用了一次 Arrays.copyOf()。
先获取独占锁,然后将原数组利用 Aaarys.copyOf() 进行拷贝,使引用 newElements指向拷贝的数组,这个 新数组的大小是原数组的大小增加1 ,然后把新增元素添加到新数组,再执行 setArray(newElements) ,使用新数组替换旧数组,setArray() 的源码如下:
final void setArray(Object[] a) {
array = a;}
需要注意的是:添加元素时,首先复制了一个快照,然后在快照上进行添加,而不是直接在原来数组上添加。
)
remove()方法:先将原数组利用 Aaarys.sort() 进行拷贝,使引用 newArray 指向拷贝的数组,并在 newArray 中删除元素,然后分两步,先把 index 之前的拷贝一次,再将 index之后的拷贝一次??? 将 array 引用指向新数组。
(改:
rremove 方法并没有先调用 Arrays.copyOf 拷贝原数组到新数组,该方法先获取 array, 赋给 elements ,然后判断要删除的元素是不是最后一个元素:如果是 ,调用 Arrays.copy(elements,len-1))替换原数;如果不是,分两次复制删除后剩余的元素到新数组 newElements ,调用的是 System.arraycopy() 方法,最后用 newElements 替换 array 。
remove 方法不像 add、set 方法,一上来就把原数组拷贝到 array 中,这是需要注意的。)
get()方法,这个方法是没有用到 ReetrantLock 的,所以不保证同步,分两步,先获取 array ,再通过下标 index 取得相应元素。
如下所示,当线程A 先获取到了 array {1,2,3},然后线程 B 拿到CPU,执行 remove ,删除掉1 ,删除的过程是:先拿到 ReetrantLock 确保只有这个线程会删除元素,然后拷贝到 newArray,删除 “1” 后再将 array 指向 {2,3},而线程 A 仍是指向的 {1,2,3}。
(补:
因此 线程A get(0) 取的元素仍是 1, 虽然 “1” 在线程 B 中已经被删除了。
这就是 写时复制策略产生的 弱一致性 问题。)
replace()方法:先取得 ReetrantLock 保证只有一个线程修改,然后取得要替换的元素 oldValue ,与替换值比较,不同则在新数组中替换掉,再赋给 array,如果相同,根据 volatile 原子性原则,需要更新,调用 setValue(object[ ] elempments)方法,体现原子性原则。该方法的实现是 :array=elempments;
。
(改:修改指定元素的方法是 set(int index,E element)))。
迭代器的弱一致性:获取迭代器后,对数组的增、删、改操作对迭代器不可见。 因为增删改都是“写时复制”,如果有线程先获取值,然后再有线程更新值,是在拷贝的那个新数组中更新的 ,而后赋给 array ,而读取值的线程读到的还是旧的 array ,所以看不到更新。
二、附源码分析
1.add方法
public boolean add(E e) {
//先获取独占锁
final ReentrantLock lock = this.lock;
lock.lock();
try {
//获取 array
Object[] elements = getArray();
int len = elements.length;
//新数组的长度是原数组的长度+1
Object[] newElements = Arrays.copyOf(elements, len + 1);
//将添加的元素加到新数组的末尾 (所以 ArrayList 就能保证按添加的顺序存放,有些算法题里会遇到。)
newElements[len] = e;
//用新数组替换原数组
setArray(newElements);
return true;
} finally {
//释放锁
lock.unlock();
}
}
2.获取指定位置元素
public E get(int index) {
return get(getArray(), index);
}
(1)先获取 array 数组
final Object[] getArray() {
return array;
}
(2)通过下标访问指定位置元素
private E get(Object[] a, int index) {
return (E) a[index];
}
3.修改指定元素
public E set(int index, E element) {
final ReentrantLock lock = this.lock;
lock.lock();
try {
Object[] elements = getArray();
//先通过 get 方法获取指定位置的元素
E oldValue = get(elements, index);
//如果指定元素与新值不一致
if (oldValue != element) {
int len = elements.length;
//新建数组
Object[] newElements = Arrays.copyOf(elements, len);
//并修改值
newElements[index] = element;
//用新数组替换原数组
setArray(newElements);
} else {
//如果指定位置元素和新值一样,体现以西 volatile 语义
// Not quite a no-op; ensures volatile write semantics
setArray(elements);
}
return oldValue;
} finally {
lock.unlock();
}
}
final void setArray(Object[] a) {
array = a;
}
4.删除元素
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();
}
}
5.弱一致性的迭代器
public Iterator<E> iterator() {
return new COWIterator<E>(getArray(), 0);
}
COWIterator 的构造方法:
static final class COWIterator<E> implements ListIterator<E> {
/** Snapshot of the array */
// array 的快照 snapshot
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 E next() {
if (! hasNext())
throw new NoSuchElementException();
return (E) snapshot[cursor++];
}
为什么说 snapshot 是 list 的快照呢? 如果在线程使用返回的迭代器遍历元素的过程中,其他线程没有对 list 进行增删改,那么 spotshot 本身就是 list 的 array ,就是引用关系;但是 如果 其他线程对 list 进行了增删改,那么 snapshot 是 list 的快照了,因为 增删改 后 list 里面的数组被新数组替换了,这时,老数组被 spotshot 引用。 这也说明,获取迭代器后,使用该迭代器元素时,其他
三、总结
CopyOnWriteArrayList 使用写时复制的策略来保证 list 的一致性,在 增删改 过程中都使用了 独占锁 ,来保证某个时刻只有一个线程能对 lsit 数组进行修改。另外 ,CopyOnWriteArrayList 提供了弱一致性的迭代器,从而保证在获取迭代器后,其他线程对 list 的修改是不可见的,迭代器遍历的数组是一个快照。