由CopyOnWriteArrayList引发的COW的几点思考

一次偶然的机会接触到了CopyOnWriteArrayList这个容器,当时就对这个名字很长的容器就产生了兴趣,后来闲暇的时候就研究了一下这个容器,下面就简单介绍一下这个容器的使用方式及其实现原理。

CpoyOnWrite(COW),写时复制,是一种应用场景很多的技术,在fork子进程,Redis数据库的持久化等等都有COW的影子,CopyOnWriteArrayList是ArrayList的线程安全体,在使用ArrayList时,如果多线程操作,遍历的时候,如果被修改了会抛出java.util.ConcurrentModificationException错误,除此之外,如果有多个线程同时对List里面的元素进行更改操作,很可能会出现我们预期之外的结果。CopyOnWriteArrayList是ArrayList在读多写少场景下的线程安全的适用版本,下面从源码的方向出发,深入解读一下这个容器的原理。

public class CopyOnWriteArrayList<E>
    implements List<E>, RandomAccess, Cloneable, java.io.Serializable {
    private static final long serialVersionUID = 8673264195747942595L;
    /** 先加个锁,保证最多只能同时存在两个list*/
    final transient ReentrantLock lock = new ReentrantLock();

    /** volatile关键字保证对array的引用的可见性 */
    private transient volatile Object[] array;
    /**set与get方法*/
    final Object[] getArray() {
        return array;
    }
    final void setArray(Object[] a) {
        array = a;
    }

    /**创建一个空的容器*/
    public CopyOnWriteArrayList() {
        setArray(new Object[0]);
    }
    ........
}

下面看一下它的包含参数的构造方法:

    public CopyOnWriteArrayList(Collection<? extends E> c) {
        Object[] elements;
        //如果二者类型一致,直接将引用赋给elementts
        if (c.getClass() == CopyOnWriteArrayList.class)
            elements = ((CopyOnWriteArrayList<?>)c).getArray();
        else {
        //如果c的类型与CopyOnWriteArrayList不一致,执行拷贝操作,将c里面包含的元素拷贝到新建的
        elements数组中
            elements = c.toArray();
            // c.toArray might (incorrectly) not return Object[] (see 6260652)
            if (elements.getClass() != Object[].class)
                elements = Arrays.copyOf(elements, elements.length, Object[].class);
        }
        setArray(elements);
    }

下面看一下add操作与remove操作:

//指定下标的add操作(如果没有指定下标默认添加到数组末尾)
    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;
            int numMoved = len - index;
            if (numMoved == 0)
                newElements = Arrays.copyOf(elements, len + 1);
            else {
            //如果指定下标满足条件,则先把index之前与之后的元素拷贝进新数组,最后把index位置的
            值置为我们指定值
                newElements = new Object[len + 1];
                System.arraycopy(elements, 0, newElements, 0, index);
                System.arraycopy(elements, index, newElements, index + 1,
                                 numMoved);
            }
            newElements[index] = element;
            setArray(newElements);
        } finally {
            lock.unlock();
        }
    }
//指定下标的remove方法,与add方法类似,都是将数组分成两个部分进行拷贝(remove必须指定下标)
    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 {
                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();
        }
    }


最主要的部分就是上面几个方法,可以看出CopyOnWriteArrayList的源码部分还是比较简单易懂,这里只是COW在集合里面的一个小应用,显而易见,这种容器的适用场景就是读多写少,在这种场景中,可以最大限度保证执行效率。

下面就介绍一下高大上的COW技术常见的应用场景:

1.最常见的就是在文件系统中使用,为了保证操作文件时不会因为突然断电等原因丢失数据,在文件系统中就采用了COW技术,在这类场景中,当我们去修改文件的时候,实际上修改的只是文件的拷贝,这时发生的读取操作还是读取的原文件内容,万一修改过程出现错误,原文件还在,不会造成太大损失。当修改完成后,替换原文件。

2.linux中创建轻量级的子进程,我们都知道,进程是操作系统中比较昂贵的资源,它具有自己的数据和程序。传统方式下,fork()函数在创建子进程时直接把所有资源复制给子进程,这种实现方式简单,但是效率低下,而且复制的资源可能对子进程毫无用处。linux为了降低创建子进程的成本,改进fork()实现方式使用COW技术创建子进程。当父进程创建子进程时,内核只为子进程创建虚拟空间,父子两个进程使用的是相同的物理空间。只有父子进程发生更改时才会为子进程分配独立的物理空间,这样就可以大幅度节约资源。

3.Redis数据库中的使用,Redis在执行持久化操作的时候,有一种RDB的持久化方式,这种方式的其中一种持久化语句是执行BGSAVE操作,BGSAVE命令会派生一个子进程,由子进程来处理RDB文件的创建工作,由父进程处理服务器请求,如果服务器并未发生数据的修改状况,父进程就与子进程共享同一个物理空间,只有在发生数据修改时,才会拷贝一份数据给子进程。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值