并发库 —— CopyOnWriteArrayList

概述

CopyOnWrite(COW)是在写操作的时候copy当前数据,然后在写完数据之后设置成新的数据。适用于读多写少的并发场景。

CopyOnWrite 使用了 ReentrantLock(支持重入的独占锁) 来支持并发操作。ReentrantLock 是一种支持重入的独占锁,任意时刻只允许一个线程来获得锁。ReentrantLock 默认是非公平锁(即不按照进入等待队列的顺序唤醒线程)机制。

本质是一种延时策略,只有在真正需要复制的时候才复制,而不是提前复制好。

源码分析

  1. 添加与读取元素
final transient ReentrantLock lock = new ReentrantLock();
final transient volatile Object[] array;

public boolean add(E e){
    final ReentrantLock lock = this.lock;
    lock.lock(); // 加锁,只允许一个线程进入
    try{
        Object[] elements = getArray();
        
        int len = elements.length;
        
        Object[] newElements = Arrays.copyof(elements, len+1); // 将原数据拷贝到一个新的数组中
        newElements[len] = e; // 插入数据
        setArray(newElements); // 将新的数组对象设回去
        return true;
    } finally {
        lock.unlock(); // 必须手动解锁
    }
}

public boolean add (int index, E e){
    final ReentrantLock lock = this.lock;
    lock.lock();
    try{
        Object[] elements = getArray();
        
        int len = elements.length;
        if(index > len || index < ){
            throw new IndexOutOfBoundsExcption("d--");
        }
        
        Obejct[] newElements;
        
        int numMoved = len - index;
        if(numMoved == 0){ // 如果插入尾部,则直接进行拷贝
            newElements = Arrays.copyOf(elements, len+1);
        } else { // 如果index位置上已经有了元素,则分两次拷贝
            newElements = new Obejct[len+1];
            // 将index之前的数据全部拷贝到新数组,不包括index
            System.arrayCopy(elements, 0, newElements, 0, index);
            // 将index之后拷贝到新数组,不包括index
            System.arrayCopy(elements, idnex, newElements, index+1, numMoved);
        }
        newElements[index] = e;
        setArray(newElements);
        
    }finally{
        lock.unlock();
    }
}

// 读取的时候并没有加同步,因此允许多个线程进入读取
public E get(int index){
    return get(getArray(), index);    
}

pulbic  E get(Object[] array, int index){
    return (E)array[index];
}

综上:在写入数据时使用ReentrantLock进行加锁,保证每一次写操作都是一个线程完成的,且每一次写操作都会先拷贝一份数据,然后再对新数组进行写操作;在读取数据的时候没有加锁,因此是允许多个线程进入访问的。所以,CopyOnWrite天生 适合读多写少的场景。

  1. 数组的遍历
private final Object[] snapshot; // 数组快照
private int cursor; 
// 遍历的时候会先获得当前数组对象的一个拷贝,即生成一个快照,接着遍历操作在快照上进行。

public Iterator<E> iterator(){
    return COWIterator<E>(getArray(),0);
}

public void testCOWIterator(){
    CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList();
    list.add("xiaoming");
    list.add("hanmeimei");
    
    Iterator<String> iterator = list.iterator();
    
    list.add("kangkang");
    
    while(iterator.hasNext()){
        syso(iterator.next());
    }
    // out xiaoming hanmeimei
}

综上:CopyOnWrite使用迭代器遍历时,我们只是获取当前list的一个快照,在对list获取迭代器后,再对list进行各种操作,是不会影响快照的遍历的,即每个线程都将获得当前数组的一个快照,所以迭代器不需要加锁就可以安全的实现遍历。

应用场景

应用于读多写少的并发场景。例如:白名单,黑名单,往往很长时间才会进行修改,大部分情况下我们只需要读数据。

总结

CopyOnWrite翻译来讲的话就是写时复制,就是我们往容器添加数据的时候不是直接添加,而是先将当前容器拷贝到新的容器中,然后往新的容器里面添加数据,最后在原容器的引用指向新的容器。因此,在并发读的情况下我们不需要加锁,因为当前的容器并不会被修改。

CopyOnWrite的优缺点:

  1. 优点:只在并发写的时候才会加锁,访问与读取不需要加锁,性能上比较高
  2. 缺点:
    1. 由于每次写数据时会复制整个数组,若容器对象占用的内存比较大时,容易发生GC。因此如果我们想用这个COW机制的话,数据不能太大,修改机会也比较少;
    2. 通过上面的源码分析,不难看出COW只能保证数据最终的一致性,不能保证数据的实时一致性。当我们碰到写入的数据需要立马读取到的话,就不要用CWO了。

最后说一句: CopyOnWriteArrayList 在修改的时候会复制整个数组,所以如果容器经常被修改或者这个数组本身就非常大的时候,是不建议使用的。反之,如果修改非常少,数组量又不大,并且对读性能要求比较高的场景,使用COW容器的效果就比较好了。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值