Java并发集合之CopyOnWriteArrayList源码解析

目录

1.CopyOnWriteArrayList

1.1整体架构

1.2新增方法源码分析

1.3删除方法的源码分析

1.4迭代方法分析


1.CopyOnWriteArrayList


1.1整体架构

  • 通过锁+数组拷贝+volatile关键字保证了线程安全
  • 每次操作都会拷贝一份数组,然后在新数组上进行操作,操作成功后在赋值回去
//一旦数组被改变就可以知道
private transient volatile Object[] array;

1.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);
        return true;
    // finally 里面释放锁,保证即使 try 发生了异常,仍然能够释放锁   
    } finally {
        lock.unlock();
    }
}

整个过程:加锁,获取数组,数组拷贝,新数组上操作,替换掉原数组,解锁

如果只是改变了数组的值是无法触发可见性的,只有拷贝新的数组,会有新的地址然后进行赋值,才可以可见!

...
// len:数组的长度、index:插入的位置(索引)
// 因为是新增索引len也可以等价为最终的数组索引
int numMoved = len - index;
// 如果要插入的位置正好等于数组的末尾,直接拷贝数组即可
if (numMoved == 0)
    newElements = Arrays.copyOf(elements, len + 1);
else {
// 如果要插入的位置在数组的中间,就需要拷贝 2 次
// 第一次从 0 拷贝到 index-1。
// 第二次从 index+1 拷贝到末尾。
    newElements = new Object[len + 1];
    System.arraycopy(elements, 0, newElements, 0, index);//0为起始位置,index在这是拷贝的数量
    System.arraycopy(elements, index, newElements, index + 1,
         numMoved);//index+1为拷贝的起始位置,numMoved也是拷贝的数量
}
// index 索引位置的值是空的,直接赋值即可。
newElements[index] = element;
// 把新数组的值赋值给数组的容器中
setArray(newElements);
...

指定位置插入:先加锁,判断是否是尾部插入,如果是尾部插入只需要直接拷贝一次,在进行赋值,新旧数组进行替换,解锁

1.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);
        //len-1才为索引位置,在减去index即为要移动的元素个数
        int numMoved = len - index - 1;
        // 如果要删除的数据正好是数组的尾部,直接删除
        if (numMoved == 0)
            setArray(Arrays.copyOf(elements, len - 1));
        else {
            // 如果删除的数据在数组的中间,
            // 从 0 拷贝到 index-1
            // 从 index-1 拷贝到数组尾部
            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();
    }
}

先获取锁,在获取数组,在得到新值需要进行返回,在通过movedNum来确定是否是尾部删除,如果是尾部删除只需要拷贝一次,如果不是尾部删除先从0拷贝到index-1,在从index+1拷贝到最后,一共两次拷贝,新旧数组进行替换,解锁,返回删除的值

ps:因为Arrays.copyOf方法只能从头开始拷贝,而Arrays.copyOfRange可以进行指定范围拷贝

批量删除:会把每一个未删除元素按顺序的放入到新数组

1.4迭代方法分析

//CopyOnWriteArrayList 类获取迭代器
public Iterator iterator() {
    return new COWIterator(getArray(), 0);
}
//CopyOnWriteArrayList 类的静态内部类
private static class COWIterator implements ListIterator {
    private final Object[] snapshot;
    private int cursor;
    //初始化只是增加了旧数组的引用而已,因为一定不会对旧数组进行修改的
    private COWIterator(Object[] elements, int initialCursor) {
        cursor = initialCursor;
        snapshot = elements;
    }
}

因为永远不会对旧数组进行更改,每次只是将引用重新指向新数组而已,

迭代进行初始化的时候是增加对旧数组的引用,如果进行更改了那么只是遍历出来是旧版本而已。

你可能感兴趣的:(JDK源码分析,java,面试)