什么是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+1newElements[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 semanticssetArray(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 )是什么?