基本介绍
概述
写时复制(英语:Copy-on-write,简称COW)是一种计算机领域的优化策略。其核心思想是,如果有多个调用者(callers)同时请求相同资源(如内存或磁盘上的数据存储),他们会共同获取相同的指针指向相同的资源,直到某个调用者试图修改资源的内容时,系统才会真正复制一份专用副本(private copy)给该调用者,而其他调用者所见到的最初的资源仍然保持不变
这过程对其他的调用者都是 [透明]的。此作法主要的优点是如果调用者没有修改该资源,就不会有副本(private copy) 被创建,因此多个调用者只是读取操作时可以共享同一份资源。
当需要修改某个共享数据时,先将原始数据复制一份,并在副本上进行修改,修改完成后再将副本的引用赋值给原始数据的引用 ,读写分离,空间换时间,避免为保证并发安全导致的激烈的锁竞争。
关键点
- Copy-on-write适用于读多写少的情况,最大程度的提高读的效率;
- Copy-on-write是最终一致性,在写的过程中,原有的读的数据是不会发生更新的,只有新的读才能读到最新数据;
- 在java中,为了能使其他线程能够及时读到新的数据,需要使用volatile变量;
- 写的时候不能并发写,需要对写操作进行加锁;
应用实现
数据库中的MVCC
多版本并发控制(MVCC) 在一定程度上实现了读写并发,它只在 可重复读(REPEATABLE READ) 和 提交读(READ COMMITTED) 两个隔离级别下工作。其他两个隔离级别都和 MVCC 不兼容,因为 未提交读(READ UNCOMMITTED),总是读取最新的数据行,而不是符合当前事务版本的数据行。而 可串行化(SERIALIZABLE) 则会对所有读取的行都加锁。
MVCC除了支持读和读并行,还支持读和写并行、写和读并行,但为了保持数据一致性,写和写是无法并行的。
行锁,并发,事务回滚等多种特性都和 MVCC 相关。MVCC 实现的核心思路就是 Copy On Write
在一个事务写的时候会copy一个记录的副本,其他事务的读操作会读取这个记录的副本,因此不影响其他事务对此记录的写入,实现写和读并行。
Java中的CopyOnWriteArrayList
CopyOnWriteArrayList 是jdk1.5以后并发包中提供的一种并发容器,写操作通过创建底层数组的新副本来实现,是一种读写分离的并发策略,我们也成为“写时复制容器”,类似的容器还有 CopyOnWriteArraySet。
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); return true; } finally { lock.unlock(); } }
CopyOnWriteArrayList底层实现添加的原理是先copy出一个容器(可以简称副本),再往新的容器里添加这个新的数据,最后把新的容器的引用地址赋值给了之前那个旧的的容器地址,但是在添加这个数据的期间,其他线程如果要去读取数据,仍然是读取到旧的容器里的数据。
读操作性能很高,因为无需任何同步措施,比较适用于读多写少的并发场景。Java 的 list 在遍历时,若中途有其他线程对容器进行修改,则会抛出ConcurrentModificationException 异常。而CopyOnWriteArrayList由于其“读写分离”的思想,遍历和修改操作分别作用在不同的 list容器,所以迭代的时候不会抛出 ConcurrentModificationExecption 异常了。
其存在数据一致性问题:CopyOnWrite容器只能保证数据的最终一致性,不能保证数据的实时一致性。如果希望写入的的数据,马上能读到,不要使用CopyOnWrite容器
Nacos避免并发读写冲突问题
Nacos在更新实例列表时,会采用CopyOnWrite技术,首先将旧的实例列表拷贝一份,然后更新拷贝的实例列表,再用更新后的实例列表来覆盖旧的实例列表。
这样在更新的过程中,就不会对读实例列表的请求产生影响,也不会出现脏读问题了。