CopyOnWriteArrayList源码解析
1.概览:
1.1简介:
 CopyOnWriteArrayList和ArrayList的底层数据结构一样,也是个数组,采用volitale关键字修饰,一旦数组被更改,其他线程能立马被感知到。CopyOnWriteArrayList是一个写时复制的容器,对容器中的元素进行增删改时,不在当前容器上进行操作,而是复制一个新的的Object数组newElements,在新的容器内进行增删改。操作后再将原容器的引用指向新的容器 setArray(newElements);
  这样做的好处是可以对 CopyOnWriteArrayList 容器的进行并发的读,而不需要加锁,因为当前容器不会添加任何新的元素。
  所以说 CopyOnWriteArrayList是一种读写分离的思想,读和写在不同的容器。
1.2属性:
//用来给CopyOnWriteArrayList的写操作加锁
final transient ReentrantLock lock = new ReentrantLock();
//该数组用volitale关键字修饰,一旦数组被修改,其它线程立马能够感知到
private transient volatile Object[] array;
2.重要方法源码解析:
2.1修改方法:
修改制定索引的元素
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 {
            //相同,就不进行修改。原容器的引用不变
            // Not quite a no-op; ensures volatile write semantics
            setArray(elements);
        }
        return oldValue;
    } finally {
        lock.unlock();
    }
}- 为啥要用volitale关键字修饰了,还要拷贝数组呢?
 
- volatile 关键字修饰的是数组,如果我们简单的在原来数组上修改其中某几个元素的值,是无法触发可见性的,我们必须通过修改数组的内存地址才行,也就说要对数组进行重新赋值才行。
 - 在新的数组上进行拷贝,对老数组没有任何影响,只有新数组完全拷贝完成之后,外部才能访问到,降低了在赋值过程中,老数组数据变动的影响
 
2.2新增方法:
直接添加:
public boolean add(E e) {
    final ReentrantLock lock = this.lock;
    //对添加操作加锁
    lock.lock();
    try {
        //得到原数组
        Object[] elements = getArray();
        int len = elements.length;
        //拷贝一个新数组,容量比原数组大1,并将原数组复制到新数组中
        Object[] newElements = Arrays.copyOf(elements, len + 1);
        //将新值放到数组的尾部
        newElements[len] = e;
        //将原容器的引用指向新数组
        setArray(newElements);
        //添加成功返回true
        return true;
    } finally {
        lock.unlock();
    }
}按索引进行添加:
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;
            // len:数组的长度、index:插入的位置
            int numMoved = len - index;
            // 如果要插入的位置正好等于数组的末尾,直接拷贝数组即可
            if (numMoved == 0)
                newElements = Arrays.copyOf(elements, len + 1);
            else {
                // 如果要插入的位置在数组的中间,就需要拷贝 2 次
                // 第一次从 0 拷贝到 index。
                // 第二次从 index+1 拷贝到末尾。
                newElements = new Object[len + 1];
                System.arraycopy(elements, 0, newElements, 0, index);
                System.arraycopy(elements, index, newElements, index + 1,
                        numMoved);
            }
            // index 索引位置的值是空的,直接赋值即可。
            newElements[index] = element;
            // 把新数组的值赋值给数组的容器中
            setArray(newElements);
        } finally {
            lock.unlock();
        }
    }从 add 系列方法可以看出,CopyOnWriteArrayList 通过加锁 + 数组拷贝+ volatile 来保证了线程安全,每一个要素都有着其独特的含义:
- 加锁:保证同一时刻数组只能被一个线程操作;
 - 数组拷贝:保证数组的内存地址被修改,修改后触发 volatile 的可见性,其它线程可以立马知道数组已经被修改;
 - volatile:值被修改后,其它线程能够立马感知最新值。
 
2.3删除方法:
删除某个索引位置的数据
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 {
            // 如果删除的数据在数组的中间,分三步走
            // 1. 设置新数组的长度减一,因为是减少一个元素
            // 2. 从 0 拷贝到数组新位置
            // 3. 从新位置拷贝到数组尾部
            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();
    }
}按值删除
public boolean remove(Object o) {
    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: {
                int prefix = Math.min(index, len);
                for (int i = 0; i < prefix; i++) {
                    if (current[i] != snapshot[i] && eq(o, current[i])) {
                        index = i;
                        break findIndex;
                    }
                }
                if (index >= len)
                    return false;
                if (current[index] == o)
                    break findIndex;
                index = indexOf(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;
        } finally {
            lock.unlock();
        }
    }批量删除包含在 c 中的元素
我们并不会直接对数组中的元素进行挨个删除,而是先对数组中的值进行循环判断,把我们不需要删除的数据放到临时数组中,最后临时数组中的数据就是我们不需要删除的数据。
ArrayList 的批量删除的思想也是和这个类似的,所以我们在需要删除多个元素的时候,最好都使用这种批量删除的思想,而不是采用在 for 循环中使用单个删除的方法,单个删除的话,在每次删除的时候都会进行一次数组拷贝(删除最后一个元素时不会拷贝),很消耗性能,也耗时,会导致加锁时间太长,并发大的情况下,会造成大量请求在等待锁,这也会占用一定的内存。
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;
        // 说明数组有值,数组无值直接返回 false
        if (len != 0) {
            // newlen 表示新数组的索引位置,新数组中存在不包含在 c 中的元素
            int newlen = 0;
            Object[] temp = new Object[len];
            // 循环,把不包含在 c 里面的元素,放到新数组中
            for (int i = 0; i < len; ++i) {
                Object element = elements[i];
                // 不包含在 c 中的元素,从 0 开始放到新数组中
                if (!c.contains(element))
                    temp[newlen++] = element;
            }
            // 拷贝新数组,变相的删除了不包含在 c 中的元素
            if (newlen != len) {
                setArray(Arrays.copyOf(temp, newlen));
                return true;
            }
        }
        return false;
    } finally {
        lock.unlock();
    }
}2.4查找
indexOf方法
// o:我们需要搜索的元素
// elements:我们搜索的目标数组
// index:搜索的开始位置
// fence:搜索的结束位置
private static int indexOf(Object o, Object[] elements,
int index, int fence) {
// 支持对 null 的搜索
if (o == null) {
for (int i = index; i < fence; i++)
// 找到第一个 null 值,返回下标索引的位置
if (elements[i] == null)
return i;
} else {
// 通过 equals 方法来判断元素是否相等
// 如果相等,返回元素的下标位置
for (int i = index; i < fence; i++)
if (o.equals(elements[i]))
return i;
}
return -1;
}
2.5迭代
// 1. 返回的迭代器是COWIterator
public Iterator<E> iterator() {
return new COWIterator<E>(getArray(), 0);
}
// 2. 迭代器的成员属性
private final Object[] snapshot;
private int cursor;
// 3. 迭代器的构造方法
private COWIterator(Object[] elements, int initialCursor) {
cursor = initialCursor;
snapshot = elements;
}
// 4. 迭代器的方法...
public E next() {
if (! hasNext())
throw new NoSuchElementException();
return (E) snapshot[cursor++];
}
//.... 可以发现的是,迭代器所有的操作都基于snapshot数组,而snapshot是传递进来的array数组
CopyOnWriteArrayList在使用迭代器遍历的时候,操作的都是原数组!因此遍历时不用调用者显式加锁。
3.补充和小结:
3.1CopyOnWriteArrayList优点:
CopyOnWriteArrayList 在写操作的同时允许读操作,大大提高了读操作的性能,因此很适合读多写少的应用场景。
3.2CopyOnWriteArrayList缺点:
- 内存占用:如果CopyOnWriteArrayList经常要增删改里面的数据,经常要执行
add()、set()、remove()的话,那是比较耗费内存的。 
- 因为我们知道每次
add()、set()、remove()这些增删改操作都要复制一个数组出来。 
- 数据一致性:CopyOnWrite容器只能保证数据的最终一致性,不能保证数据的实时一致性。
 
- 从上面的例子也可以看出来,比如线程A在迭代CopyOnWriteArrayList容器的数据。线程B在线程A迭代的间隙中将CopyOnWriteArrayList部分的数据修改了(已经调用
setArray()了)。但是线程A迭代出来的是原有的数据。 
3.3CopyOnWriteSet
CopyOnWriteArraySet的原理就是CopyOnWriteArrayList。
private final CopyOnWriteArrayList<E> al;
public CopyOnWriteArraySet() {
al = new CopyOnWriteArrayList<E>();
}
                










