Java里String这个类在实现replace()方法的时候,并没有更改元字符串里面的value[]数组的内容,而是创建了一个新的字符串,这种方法在解决不可变对象的修改问题时经常用到。这本质上是一种Copy-on-Write方法-写时复制。
不可变对象的写操作往往都是使用Copy-on-Write方法解决的,但Copy-on-Write的应用领域并不局限于Immutability模式。
Copy-on-Write模式的应用领域
并发容器CopyOnWriteArrayList和CopyOnWriteArraySet这两个Copy-on-Write容器,它们背后的设计思想是Copy-on-Write;通过Copy-on-Write这两个容器实现的读操作是无锁的,由于无锁,所以将读操作的性能发挥到了极致。
操作系统领域,类Unix的操作系统中创建进程的API是fork(),传统的fork()函数会创建父进程的一个完整副本,如父进程的地址空间现在用到了1G的内存,那么fork()子进程的时候要复制父进程整个进程的地址空间给子进程,这个过程是很耗时的。
而Linux中的fork()函数,fork()子进程的时候,并不复制整个进程的地址空间,而是让父子进程共享同一个地址空间;只用在父进程或者子进程需要写入的时候才会复制地址空间,从而使父子进程拥有各自的地址空间。
本质上讲,父子进程的地址空间以及数据都是需要隔离的,使用Copy-on-Write更多的体现的是一种延时策略,只有在真正需要复制的时候才复制,而不是提前复制好,同时Copy-on-Write还支持按需复制,所以Copy-on-Write在操作系统领域是能够提升性能的。
相比较而言,Java提供的Copy-on-Write容器,由于在修改的同时会复制整个容器,所以在提升读操作性能的同时,是以内存复制为代价的。同样是应用Copy-on-Write,不同的场景,对性能的影响是不同的。
在操作系统领域,除了创建进程用到了Copy-on-Write,很多文件系统也同样用到了,如Btrfs(B-Tree File System)、aufs(advanced multi-layered unification filesystem)等。
其它领域,Docker容器镜像的设计是Copy-on-Write;分布式源码管理系统Git背后的设计思想也有Copy-on-Write。
Copy-on-Write最大的应用领域还是在函数式编程领域。函数式编程的基础是不可变性(Immutability),所以函数式编程里面所有的修改操作都需要Copy-on-Write来解决。所有数据的修改都需要复制一份,性能会成为瓶颈。Copy-on-Write可以按需要复制,减小性能压力。
CopyOnWriteArrayList和CopyOnWriteArraySet这两个Copy-on-Write容器在修改的时候会复制整个数组,如果容器经常被修改或者这个数组本省就非常大的时候,是不建议使用的。相反的情况,并且对读性能要求苛刻的场景,使用Copy-on-Write容器效果非常好。
分布式服务客户端的路由表
服务提供方上线、下线(频率低),利用Immutability模式创建新的路由对象或者删除对应的路由对象。
总结
Copy-on_Write是最简单的并发解决方案。Java中的基本数据类型String、Integer、Long等都是基于Copy-On-Write方案实现的。
Copy-on-Write是一项非常通用的技术方案,在很多领域都有着广泛的应用。
缺点是消耗内存,每次修改都需要复制一个新的对象出来。
Java提供了CopyOnWriteArrayList,没有提供CopyOnWriteLinkedList。
ArrayList数组,在内存上是一块连续的区域,拷贝效率比较高。LinkedList链表通过指针串联,不是连续的区域,拷贝时必须进行遍历操作,效率低,完整的复制LinkedList链表性能开销太大。(链表适合写多的场景,且可以使用分段锁、节点锁,没有写时复制的必要)