CopyOnWriteArrayList源码解析学习总结

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关键字修饰了,还要拷贝数组呢?
  1. volatile 关键字修饰的是数组,如果我们简单的在原来数组上修改其中某几个元素的值,是无法触发可见性的,我们必须通过修改数组的内存地址才行,也就说要对数组进行重新赋值才行。
  2. 在新的数组上进行拷贝,对老数组没有任何影响,只有新数组完全拷贝完成之后,外部才能访问到,降低了在赋值过程中,老数组数据变动的影响

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 来保证了线程安全,每一个要素都有着其独特的含义:

  1. 加锁:保证同一时刻数组只能被一个线程操作;
  2. 数组拷贝:保证数组的内存地址被修改,修改后触发 volatile 的可见性,其它线程可以立马知道数组已经被修改;
  3. 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>();
    }

参考:https://www.imooc.com/read/47/article/857

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值