在上一篇文章中我们讲到 Java 里 String 这个类在实现 replace() 方法的时候,并没有更改原字符串里面 value[] 数组的内容,而是创建了一个新字符串,这种方法在解决不可变对象的修改问题时经常用到。如果你深入地思考这个方法,你会发现它本质上是一种Copy-on-Write 方法。所谓 Copy-on-Write,经常被缩写为 COW 或者 CoW,顾名思义就是写时复制。

不可变对象的写操作往往都是使用 Copy-on-Write 方法解决的,当然 Copy-on-Write 的应用领域并不局限于 Immutability 模式。下面我们先简单介绍一下 Copy-on-Write 的应用领域,让你对它有个更全面的认识。

Copy-on-Write 模式的应用领域

我们知道 CopyOnWriteArrayList 和 CopyOnWriteArraySet 这两个 Copy-on-Write 容器,它们背后的设计思想就是 Copy-on-Write;通过 Copy-on-Write 这两个容器实现的读操作是无锁的,由于无锁,所以将读操作的性能发挥到了极致。

除了上面我们说的 Java 领域,很多其他领域也都能看到 Copy-on-Write 的身影:Docker 容器镜像的设计是 Copy-on-Write,甚至分布式源码管理系统 Git 背后的设计思想都有 Copy-on-Write。

CopyOnWriteArrayList 和 CopyOnWriteArraySet 这两个 Copy-on-Write 容器在修改的时候会复制整个数组,所以如果容器经常被修改或者这个数组本身就非常大的时候,是不建议使用的。反之,如果是修改非常少、数组数量也不大,并且对读性能要求苛刻的场景,使用 Copy-on-Write 容器效果就非常好了。

一个真实案例

RPC 框架中有个基本的核心功能就是负载均衡。服务提供方是多实例分布式部署的,所以服务的客户端在调用 RPC 的时候,会选定一个服务实例来调用,这个选定的过程本质上就是在做负载均衡,而做负载均衡的前提是客户端要有全部的路由信息。

例如在下图中,A 服务的提供方有 3 个实例,分别是 192.168.1.1、192.168.1.2 和 192.168.1.3,客户端在调用目标服务 A 前,首先需要做的是负载均衡,也就是从这 3 个实例中选出 1 个来,然后再通过 RPC 把请求发送选中的目标实例。

RPC 路由关系图

RPC 框架的一个核心任务就是维护服务的路由关系,我们可以把服务的路由关系简化成下图所示的路由表。当服务提供方上线或者下线的时候,就需要更新客户端的这张路由表。

每次 RPC 调用都需要通过负载均衡器来计算目标服务的 IP 和端口号,而负载均衡器需要通过路由表获取接口的所有路由信息,也就是说,每次 RPC 调用都需要访问路由表,所以访问路由表这个操作的性能要求是很高的。不过路由表对数据的一致性要求并不高,一个服务提供方从上线到反馈到客户端的路由表里,即便有 5 秒钟,很多时候也都是能接受的。而且路由表是典型的读多写少类问题。

通过以上分析,你会发现一些关键词:对读的性能要求很高,读多写少,弱一致性。它们综合在一起,你会想到什么呢?CopyOnWriteArrayList 和 CopyOnWriteArraySet 天生就适用这种场景啊。所以下面的示例代码中,RouteTable 这个类内部我们通过ConcurrentHashMap>这个数据结构来描述路由表,ConcurrentHashMap 的 Key 是接口名,Value 是路由集合,这个路由集合我们用是 CopyOnWriteArraySet。

下面我们再来思考 Router 该如何设计,服务提供方的每一次上线、下线都会更新路由信息,这时候你有两种选择。

一种是通过更新 Router 的一个状态位来标识,如果这样做,那么所有访问该状态位的地方都需要同步访问,这样很影响性能。

另外一种就是采用 Immutability 模式,每次上线、下线都创建新的 Router 对象或者删除对应的 Router 对象。由于上线、下线的频率很低,所以后者是最好的选择。

Router 的实现代码如下所示,是一种典型 Immutability 模式的实现,需要你注意的是我们重写了 equals 方法,这样 CopyOnWriteArraySet 的 add() 和 remove() 方法才能正常工作。

// 路由信息

public final class Router{

private final String ip;

private final Integer port;

private final String iface;

// 构造函数

public Router(String ip,

Integer port, String iface){

this.ip = ip;

this.port = port;

this.iface = iface;

}

// 重写 equals 方法

public boolean equals(Object obj){

if (obj instanceof Router) {

Router r = (Router)obj;

return iface.equals(r.iface) &&

ip.equals(r.ip) &&

port.equals(r.port);

}

return false;

}

public int hashCode() {

// 省略 hashCode 相关代码

}

}

// 路由表信息

public class RouterTable {

//Key: 接口名

//Value: 路由集合

ConcurrentHashMap>

rt = new ConcurrentHashMap<>();

// 根据接口名获取路由表

public Set get(String iface){

return rt.get(iface);

}

// 删除路由

public void remove(Router router) {

Set set=rt.get(router.iface);

if (set != null) {

set.remove(router);

}

}

// 增加路由

public void add(Router router) {

Set set = rt.computeIfAbsent(

route.iface, r ->

new CopyOnWriteArraySet<>());

set.add(router);

}

}

总结

其实 Copy-on-Write 才是最简单的并发解决方案。它是如此简单,以至于 Java 中的基本数据类型 String、Integer、Long 等都是基于 Copy-on-Write 方案实现的。

Copy-on-Write 是一项非常通用的技术方案,在很多领域都有着广泛的应用。不过,它也有缺点的,那就是消耗内存,每次修改都需要复制一个新的对象出来。如果写操作非常少,那你就可以尝试用一下 Copy-on-Write,效果还是不错的。

java copy-on-write_[Java并发-18-并发设计模式] COW模式:Copy-on-Write模式的应用领域相关推荐

  1. Java并发编程-并发工具包(java.util.concurrent)使用指南(全)

    1. java.util.concurrent - Java 并发工具包 Java 5 添加了一个新的包到 Java 平台,java.util.concurrent 包.这个包包含有一系列能够让 Ja ...

  2. java list 占用内存不释放_Java并发编程 - CopyOnWrite容器类

    前言 当我们对List进行遍历的时候,如果list被修改了会抛出java.util.ConcurrentModificationException错误.那么有没有办法在遍历一个list的时候,还向li ...

  3. 一篇博客带你轻松应对java面试中的多线程与高并发

    1. Java线程的创建方式 (1)继承thread类 thread类本质是实现了runnable接口的一个实例,代表线程的一个实例.启动线程的方式start方法.start是一个本地方法,执行后,执 ...

  4. Java并发编程-并发工具包java.util.concurrent使用指南

    译序 本指南根据 Jakob Jenkov 最新博客翻译,请随时关注博客更新 本指南已做成中英文对照阅读版的 pdf 文档,有兴趣的朋友可以去 Java并发工具包java.util.concurren ...

  5. java await signal_【Java并发008】原理层面:ReentrantLock中 await()、signal()/signalAll()全解析...

    一.前言 上篇的文章中我们介绍了AQS源码中lock方法和unlock方法,这两个方法主要是用来解决并发中互斥的问题,这篇文章我们主要介绍AQS中用来解决线程同步问题的await方法.signal方法 ...

  6. java 多线程操作map_Java 多线程中ConcurrentHashMap并发读写操作范例

    范例1: package com.contoso; import java.util.Random; import java.util.UUID; import java.util.concurren ...

  7. java 并发(并发工具包)

    java 并发(并发工具包) ##13个原子操作类 ####原子基本类型 AtomicBoolean AtomicInteger AtomicLong 常用方法如下: int addAndGet(in ...

  8. 【2022最新Java面试宝典】—— Java并发编程面试题(123道含答案)

    目录 一.基础知识 1. 为什么要使用并发编程 2. 多线程应用场景 3. 并发编程有什么缺点 4. 并发编程三个必要因素是什么? 5. Java 程序中怎么保证多线程的运行安全? 6. 并行和并发有 ...

  9. java fork_浅谈Java的Fork/Join并发框架

    前几天有写到整合并发结果的文章,于是联想到了Fork/Join.因为在我看来整合并发结果其实就是Fork/Join中的Join步骤.所以今天我就把自己对Fork/Join一些浅显的理解记录下来. 1. ...

最新文章

  1. C++中一些类和数据结构的大小的总结
  2. python编程 语言-Python——最美丽的编程语言
  3. 如果把线程当作一个人来对待,所有问题都瞬间明白了
  4. mongodb防火墙配置
  5. [转载] Python numpy函数:all()和any()比较矩阵
  6. linux安装svn(yum安装)
  7. 《深入浅出数据分析》资源汇总
  8. 概率论的学习和整理11:伯努利试验的3种分布:0-1分支,几何分布, 二项分布
  9. Silverlight新型的富媒体
  10. Overlay网络与物理网络的关系
  11. Python学习培训方法
  12. ips细胞技术治疗尿毒症最新进展
  13. Google VR开发-Cardboard VR SDK头部追踪实现(罗德里格旋转公式)
  14. 谷歌小恐龙作弊+作死方法
  15. 来自菜鸡的前端权限简单实现
  16. 名博是怎样炼成的——读后感
  17. 5G的五项核心技术和5.5G相关的技术
  18. PAT 乙级 1086 python
  19. 后端接口返回一张图片
  20. Vite打包项目提示“some chunks are larger than 500 kib....“

热门文章

  1. pandas Dataframe/Series 设置保留小数位数
  2. 关于php的函数,总结关于PHP文件函数有哪些
  3. java 证书公钥 私钥_java#keytool#生成私钥证书库、公钥证书库
  4. redis 参数配置总结
  5. 【C++对象模型】第一章 关于对象
  6. Velocity教程 (zhuan)
  7. mysql查询锁表及解锁
  8. (转)Flex4中的皮肤(2):Skin State
  9. tika提取html,TIKA内容提取
  10. matlab乘幂的指数是矩阵,信号与系统MATLAB基本语法.ppt