解析CopyOnWrite机制 以java的CopyOnWriteArrayList为例

什么是CopyOnWrite

写时复制(Copy-on-write,简称COW)是读写分离的一种实现方式,因为读和写在不同的容器中。

核心思想:线程在修改数据的时,会将原数据复制一份,然后在副本上修改,最后再把原数据的引用(指针)更新为新数据的引用(指针)。

CopyOnwrite特点

  • 可以让读线程和写线程并发执行,读线程可以完全不加锁,能提高效率。(优点)
  • 不能保证读写过程中数据的实时一致性(只能保证弱一致性/最终一致性),因为在写入后,原本读线程读取到的数据不会马上更新(需要等本次get结束后后面的get才能读到最新的数据),只有最新的读取才是最新的数据。(缺点)
  • 需要额外的空间开销,因为每次修改都要copy一份数据(缺点)

注意:

  • 在副本上修改完内容后,并不是直接将新旧的元素值替换为的元素值,而是替换地址(引用);比如原数组int[] Data = array1,而array1={1,2,3},副本数组是array2={1,2,3},把array2修改成{3,4,5}后,直接把array1弃用掉,此时Data = array2 ,值为{3,4,5}
  • 写和写肯定还是不能并发执行的,要加互斥锁

图解CopyOnWrite流程

首先,可以有多个读线程读数据,然后只能有一个写线程修改数据,修改数据时,先copy一个副本,然后在副本上修改

在这里插入图片描述

修改完副本内容之后,把原本引用覆盖掉,此时原本的读线程如果当前读取操作没有执行完毕,则读取的还是原来的数据,而如果来了新的读线程,读取到的就是新的数据(新的内存地址)

在这里插入图片描述
如果原本读线程1、2、3刚刚的读取操作结束了,进行的新的读取,那此时读取的就一定是新的数据了
在这里插入图片描述

为什么说它不能保证数据的实时一致性?

因为如果在某一次读取过程还没执行完毕时(执行一部分),数据被修改了,那么本次读取对修改就会感知不到,只有下一次读取才是最新的数据。本次读取的数据相对于最新数据来说可能存在滞后性(时延),所以说它不能保证实时一致性,因为实时一致性要求就算你某次读取中途被修改,它也应该立刻感知到。

结合CopyOnWriteArrayList源码分析

底层是一个数组array(只能通过get和set方法访问),用volatile修饰(注意修饰的是数组引用,不是数组内容),如果这个引用被更新了,也就是执行了add、set等方法,其它线程会马上感知到这个数组的变化,后面的读取也就是新的值了

在这里插入图片描述

add方法

/**
 * Appends the specified element to the end of this list.
 *
 * @param e element to be appended to this list
 * @return {@code true} (as specified by {@link Collection#add})
 */
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);//copy一个新数组,size为当前length+1
        newElements[len] = e;//在新数组上添加元素
        setArray(newElements);//新数组覆盖旧数组(注意覆盖的是地址,不是数组内容)
        return true;
    } finally {
        lock.unlock();//解锁
    }
}

set也是类似过程,只不过copy的数组的size和原来的一样

    /**
     * Replaces the element at the specified position in this list with the
     * specified element.
     *
     * @throws IndexOutOfBoundsException {@inheritDoc}
     */
    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();//解锁
        }
    }

可以看到,传入的a是一个数组,直接把a赋值给array代表的是修改引用

    /**
     * Sets the array.
     */
    final void setArray(Object[] a) {
        array = a;
    }

get方法

    @SuppressWarnings("unchecked")
    private E get(Object[] a, int index) {
        return (E) a[index];
    }

分析:

既然volatile可以保证修改后的可见性,那为什么上面的图2中的读线程读取的还是旧数据?

因为读取方法不是一个原子操作,有可能读操作已经进入到旧的内存中去取值了,就算此时把旧的内存地址换成新的内存地址,本次读操作也感知不到了。

比如CopyOnWriteArrayList的get方法,在一次读取中分成两步

1、先读取原数组内存

2、通过下标访问该内存中指定位置的元素

如果已经执行了步骤1,还没有执行步骤2,这时候有一个写线程把数据修改了,那么执行步骤2的时候访问的就还是旧的数组内容。也就是说某一次get读取过程中数据被修改的话,有可能还是读的是旧的数据,只有下一个读取get才是最新的,这就是数据的弱一致性。

那后续读线程1、读线程2、读线程3后续再继续读数据的时候,读取到的是最新数据还是旧的数据?

是新的,因为volatile会保证更新后其它线程可见。

什么时候用

  • 读多写少的时候用,且尽量用批量修改操作,因为每次修改都会copy副本

和其它实现线程安全的手段有什么区别?

  • 用普通锁(synchronized或可重入锁)实现的是:读读互斥、读写互斥、写写互斥
  • 用读写锁实现的是:读读不互斥、读写互斥、写写互斥
  • 用copyonwrite实现的是:读读不不互斥、读写不互斥、写写互斥

参考链接:

java并发:CopyOnWrite机制

简单聊聊copy on write(写时复制)技术

CopyOnWriterArrayList 详解

Java中的copy on write(COW )是什么?

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值