写入时复制(copy on write)
作为使用频率很高的List和Set,JUC提供了相应的线程安全集合,就是Copy-On-Write。分别为CopyOnWriteArrayList和CopyOnWriteArraySet。
1、应用场景
Copy-On-Write并发容器用于读多写少的并发场景。
-
比如Nacos的注册中心同步方式,用的是copyonwrite,就是复制一份副本,写入后直接替换。注册中心的实时性并没有那么高,所以使用cow来保证数据的最终一致性即可。
-
Redis的COW
- Redis在持久化时,如果是采用BGSAVE命令或者BGREWRITEAOF的方式,那Redis会fork出一个子进程来读取数据,从而写到磁盘中。
- 总体来看,Redis还是读操作比较多。如果子进程存在期间,发生了大量的写操作,那可能就会出现很多的分页错误(页异常中断page-fault),这样就得耗费不少性能在复制上。
- 而在rehash阶段上,写操作是无法避免的。所以Redis在fork出子进程之后,将负载因子阈值提高,尽量减少写操作,避免不必要的内存写入操作,最大限度地节约内存。
-
实际运用:一个充值排行榜的功能,排行榜会有很多人查看访问,但是只有充值之后才会修改排行榜上的数据,或者充值之后也不更新,只有每天晚上9点更新排行榜,标准的读多写少。
public class CopyOnWriteArrayListTest {
public static CopyOnWriteArrayList<Integer> rankIds = new CopyOnWriteArrayList<Integer>();
public static void addRankIds(int id) {
/* * 获取id在rankIds中的排序,代码省略 * 假设id应该在排行榜中的第一个 */
rankIds.add(0, id);
}
}
2、定义
写入时复制(英语:Copy-on-write,简称COW)是一种计算机程序设计领域的优化策略。
3、原理
核心思想,如果有多个调用者(callers)同时要求相同资源(如内存或磁盘上的数据存储),他们会共同获取相同的指针指向相同的资源,直到某个调用者试图修改资源的内容时,系统才会真正复制一份专用副本(private copy)给该调用者,而其他调用者所见到的最初的资源仍然保持不变。这过程对其他的调用者都是透明的(transparently)。此作法主要的优点是如果调用者没有修改该资源,就不会有副本(private copy)被创建,因此多个调用者只是读取操作时可以共享同一份资源。
-
当读取共享数据时,直接读取,不需要有其他操作(比如阻塞等待、复制等)。读数据不会被阻塞。
get()方法是没有加锁的
public E get(int index) { return get(getArray(), index); } final Object[] getArray() { return array; } private E get(Object[] a, int index) { return (E) a[index]; }
-
当写共享数据时,将旧数据复制出来一份作为新数据,只修改新数据,修改完新数据之后将新数据的引用赋值给原来数据的引用。在整个写数据的过程中,所有读取共享数据的操作都是读的旧数据。
-
lock锁同步
-
旧数组复制出一个新数组
-
新数组添加元素
-
新数组引用赋给array
**add()**方法
public boolean add(E e) { final ReentrantLock lock = this.lock; lock.lock(); // 1. lock锁同步 try { // 2. 旧数组复制出一个新数组 Object[] elements = getArray(); int len = elements.length; Object[] newElements = Arrays.copyOf(elements, len + 1); // 3. 新数组添加元素 newElements[len] = e; // 4. 新数组引用赋给 array setArray(newElements); return true; } finally { lock.unlock();// 解锁 } }
-
-
COW容器只有写操作与写操作之间是互斥的,读读和读写都不互斥。