参考:https://www.cnblogs.com/huangjuncong/p/9160713.html
https://www.iteye.com/blog/caoyaojun1988-163-com-1754686
Java.util.concurrent包中只有CopyOnWriteArrayList一种并发LIst,且这是一个线程安全的ArrayList,对齐进行修改操作和元素迭代操作都是在底层创建一个拷贝数组数组(快照)上进行的,即写时拷贝策略
CopyOnWriteArrayList的核心思想是利用高并发往往是读多写少的特性,对读操作不加锁,对写操作,先复制一份新的集合,在新的集合上面修改,然后将新集合赋值给旧的引用,并通过volatile 保证其可见性,当然写操作的锁是必不可少的了。所以在一些地方做了一些变化(如:addIfAbsent(E e)不上锁,只有在array数组中查询不到e的时候再调用addIfAbsent(E e, Object[] snapshot)。大致过程是 大致执行过程:首先获取当前数组存取元素,然后找我们要插入数据的下标值,找到了直接返回false,没找到的话,上锁,再次判断数组是否发生变化,这里多一次判断,我认为是为了防止这种可能性的出现:在判断完是否存在元素和加锁之间,另一个线程加入了我们要加的元素,不判断的话,那么同一个元素就有可能出现俩次,违背了addIfAbsent的意愿。加锁之后,如果数组并没有变的话,那么就执行CopyOnWriteArrayList的老套路,将原来的数据放到一个比原来长度长1的数组,最后一个空位放我们要放的数据。通过上述过程就实现了数据的唯一性,CopyOnWriteSet底层实现就是利用CopyOnWriteArrayList,唯一性就是利用addifabsent方法。
CopyOnWriteArrayList的类有哪些属性和方法,大致如下图所示(还有很多未进行展示):
首先,该类有一个Object类型数组来存放具体元素,且只能有getArray()和setArray()来访问
然后还有一个ReentrantLock(可重入锁)独占锁对象来保证只有一个线程对array进行修改,即同时只有一个线程可以获取就可以了
如果让我们自己去做一个写时拷贝的线程安全的LIst,我们应该怎么做,要考虑哪些点?
1.list何时初始化,初始化list元素个数为多少,list是有限大小?
2.如何保证线程安全?
3.如何使用迭代器遍历list时候的数据一致性?
CopyOnWriteArrayList源码分离如下:
1.初始化,首先看一下CopyOnWriteArrayList的无参构造函数,如下代码内部创建了一个大小为0的Object数组作为array的初始值。源码如下:
/**
* Creates an empty list.
*/
public CopyOnWriteArrayList() {
setArray(new Object[0]);
}
接下来再看看CopyOnWriteArrayList的有参构造函数,源码如下:
/**
* 入参为集合,拷贝集合里面元素到本list
*
* @param c the collection of initially held elements
* @throws NullPointerException if the specified collection is null
*/
public CopyOnWriteArrayList(Collection extends E> c) {
Object[] es;
if (c.getClass() == CopyOnWriteArrayList.class)
es = ((CopyOnWriteArrayList>)c).getArray();
else {
es = c.toArray();
// defend against c.toArray (incorrectly) not returning Object[]
// (see e.g. https://bugs.openjdk.java.net/browse/JDK-6260652)
if (es.getClass() != Object[].class)
es = Arrays.copyOf(es, es.length, Object[].class);
}
setArray(es);
}
/**
* 创建一个list,其内部元素是入参toCopyIn的拷贝
*
* @param toCopyIn the array (a copy of this array is used as the
* internal array)
* @throws NullPointerException if the specified array is null
*/
public CopyOnWriteArrayList(E[] toCopyIn) {
setArray(Arrays.copyOf(toCopyIn, toCopyIn.length, Object[].class));
}
2.添加元素,CopyOnWriteArrayList中添加元素函数有add(E e) , add(int index , E element) ,addIfAbsent(E e) , addAllAbsent(Collection extends E> c) 等操作,原理一致
接下来就以add(int index , E element) 进行讲解。源码如下:
/**
* 如果不存在则添加,这里只有读操作,所以不上锁,具体的写操作在addIfAbsent(E e, Object[] snapshot)方法里
*
* @param e element to be added to this list, if absent
* @return {@code true} if the element was added
*/
public boolean addIfAbsent(E e) {
Object[] snapshot = getArray();
return indexOfRange(e, snapshot, 0, snapshot.length) < 0
&& addIfAbsent(e, snapshot);
}
/**
* 加锁之后再次判断,以防在这期间有线程执行了写操作
*/
private boolean addIfAbsent(E e, Object[] snapshot) {
synchronized (lock) {
Object[] current = getArray();
int len = current.length;
//两次若不一样,说明数组已经改变
if (snapshot != current) {
// 找到前后两次数组长度的最小值
int common = Math.min(snapshot.length, len);
//先在0-common中比较两个数组,比较有出入的地方,存在e则返回false,再比较common-len,存在(返回值>=0)则返回false
for (int i = 0; i < common; i++)
if (current[i] != snapshot[i]
&& Objects.equals(e, current[i]))
return false;
if (indexOfRange(e, current, common, len) >= 0)
return false;
}
//老套路,数组拷贝,新数组添加元素,指向array
Object[] newElements = Arrays.copyOf(current, len + 1);
newElements[len] = e;
setArray(newElements);
return true;
}
}
3.获取指定位置元素,E get(int index)获取下标为index的元素,如果元素不存在会抛出IndexOutOfBoundsException 异常,源码如下:
public E get(int index) {
return get(getArray(), index);
}
final Object[] getArray() {
return array;
}
如上代码,获取指定位置的元素分为两步,首先获取到当前list里面的array数组,这里称为步骤1,然后通过随机访问的下标方式访问指定位置的元素,这里称为步骤2。
从代码中可以看到整个过程并没有加锁,这就可能会导致当执行完步骤1后执行步骤2前,另外一个线程C进行了修改操作,比如remove操作,就会进行写时拷贝删除当前get方法要访问的元素,并且修改当前list的array为新数组。
而这之后步骤2 可能才开始执行,步骤2操作的是线程C删除元素前的一个快照数组(因为步骤1让array指向的是原来的数组),所以虽然线程C已经删除了index处的元素,但是步骤2还是返回index处的元素,这其实就是写时拷贝策略带来弱一致性。
4.修改指定元素,修改 list 中指定元素的值,如果指定位置的元素不存在则抛出 IndexOutOfBoundsException 异常,源码码如下:
/**
* Replaces the element at the specified position in this list with the specified element.
*
* @throws IndexOutOfBoundsException {@inheritDoc}
*/
public E set(int index, E element) {
synchronized (lock) {
Object[] es = getArray();
E oldValue = elementAt(es, index);
if (oldValue != element) {
es = es.clone();
es[index] = element;
setArray(es);
}
return oldValue;
}
}
如上代码,首先获取了独占锁控制了其他线程对array数组的修改,然后获取当前数组,并调用get方法获取指定位置元素。
如果指定的位置元素与新值不一致则创建新数组并拷贝元素,在新数组上修改指定位置元素值并设置新数组到array。
5.删除元素,删除list里面指定的元素,主要的方法有如下方法:
E remove(int index)
boolean remove(Object o)
boolean remove(Object o, Object[] snapshot, int index) 等方法,原理一致,这里讲解下 remove(int index) 方法,源码如下:
public E remove(int index) {
synchronized (lock) {
Object[] es = getArray();
int len = es.length;
E oldValue = elementAt(es, index);
int numMoved = len - index - 1;
Object[] newElements;
if (numMoved == 0)
newElements = Arrays.copyOf(es, len - 1);
else {
newElements = new Object[len - 1];
System.arraycopy(es, 0, newElements, 0, index);
System.arraycopy(es, index + 1, newElements, index,
numMoved);
}
setArray(newElements);
return oldValue;
}
}
public boolean remove(Object o) {
Object[] snapshot = getArray();
int index = indexOfRange(o, snapshot, 0, snapshot.length);
return index >= 0 && remove(o, snapshot, index);
}
/**
* A version of remove(Object) using the strong hint that given
* recent snapshot contains o at the given index.
*/
private boolean remove(Object o, Object[] snapshot, int index) {
synchronized (lock) {
Object[] current = getArray();
int len = current.length;
if (snapshot != current) findIndex: {
int prefix = Math.min(index, len);
for (int i = 0; i < prefix; i++) {
if (current[i] != snapshot[i]
&& Objects.equals(o, current[i])) {
index = i;
break findIndex;
}
}
if (index >= len)
return false;
if (current[index] == o)
break findIndex;
index = indexOfRange(o, current, index, len);
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;
}
}
正如上面代码所示,其实和新增元素时候是类似的,首先是获取独占锁保证删除数组期间,其他线程不能对array进行修改,然后获取数组中要给删除的元素,并把剩余的原始拷贝到新数组后,把新数组替换原来的数组,最后在返回前释放锁。
6.接下来要讲解一下弱一致性的迭代器。
遍历列表元素可以使用迭代器进行迭代操作,讲解什么是迭代器的弱一致性前先上一个例子说明下迭代器的使用。代码如下:
import java.util.concurrent.CopyOnWriteArrayList;
public class CopyOnWriteArrayListTest {
public static void main( String[] args ) {
CopyOnWriteArrayList arrayList = new CopyOnWriteArrayList<>();
arrayList.add("hello");
arrayList.add("java");
Iterator itr = arrayList.iterator();
while(itr.hasNext()){
System.out.println(itr.next());
}
}
}
运行结果如下:
其中迭代器的hasNext方法用来判断是否还有元素,next方法则是具体返回元素。那么接下来看CopyOnWriteArrayList中迭代器是弱一致性,所谓弱一致性是指返回迭代器后,其他线程对list的增删改对迭代器不可见,无感知的,那么下面就看看是如何做到的。源码如下:
/**
* Returns an iterator over the elements in this list in proper sequence.
*
* The returned iterator provides a snapshot of the state of the list
* when the iterator was constructed. No synchronization is needed while
* traversing the iterator. The iterator does NOT support the
* {@code remove} method.
*
* @return an iterator over the elements in this list in proper sequence
*/
public Iterator iterator() {
return new COWIterator(getArray(), 0);
}
static final class COWIterator implements ListIterator {
/** array的快照版本 */
private final Object[] snapshot;
/** Index of element to be returned by subsequent call to next. */
//数组下标
private int cursor;
COWIterator(Object[] es, int initialCursor) {
cursor = initialCursor;
snapshot = es;
}
//是否遍历结束(正向)
public boolean hasNext() {
return cursor < snapshot.length;
}
//反向遍历
public boolean hasPrevious() {
return cursor > 0;
}
@SuppressWarnings("unchecked")
public E next() {
if (! hasNext())
throw new NoSuchElementException();
return (E) snapshot[cursor++];
}
@SuppressWarnings("unchecked")
public E previous() {
if (! hasPrevious())
throw new NoSuchElementException();
return (E) snapshot[--cursor];
}
public int nextIndex() {
return cursor;
}
public int previousIndex() {
return cursor - 1;
}
如上代码当调用iterator()方法获取迭代器时候实际是返回一个COWIterator对象,COWIterator的snapshot变量保存了当前list的内容,cursor是遍历list数据的下标。
这里为什么说snapshot是list的快照呢?明明是指针传递的引用,而不是拷贝。如果在该线程使用返回的迭代器遍历元素的过程中,其他线程没有对list进行增删改,那么snapshot本身就是list的array,因为它们是引用关系。
但是如果遍历期间,有其他线程对该list进行了增删改,那么snapshot就是快照了,因为增删改后list里面的元素被新数组替换了,这时候老数组只有被snapshot索引用,所以这也就说明获取迭代器后,使用改迭代器进行变量元素时候,其它线程对该list进行的增删改是不可见的,因为它们操作的是两个不同的数组,这也就是弱一致性的达成。
注意:CopyOnWriteArrayList使用写时拷贝的策略来保证list的一致性,而获取-拷贝-写入 三步并不是原子性的,所以在修改增删改的过程中都是用了独占锁,并保证了同时只有一个线程才能对list数组进行修改。
另外CopyOnWriteArrayList提供了弱一致性的迭代器,保证在获取迭代器后,其他线程对list的修改该不可见,迭代器遍历时候的数组是获取迭代器时候的一个快照,另外并发包中CopyOnWriteArraySet 底层就是使用它进行实现,感兴趣的可以去翻翻看。