java并发编程:CopyOnWrite容器介绍

2023-11-11

前言

Copy-On-Write简称COW,是一种用于程序设计中的优化策略。其基本思路是,从一开始大家都在共享同一个内容,当某个人想要修改这个内容的时候,才会真正把内容Copy出去形成一个新的内容然后再改,这是一种延时懒惰策略。从JDK1.5开始Java并发包里提供了两个使用CopyOnWrite机制实现的并发容器,它们是CopyOnWriteArrayList和CopyOnWriteArraySet。CopyOnWrite容器非常有用,可以在非常多的并发场景中使用到。

什么是CopyOnWrite容器

CopyOnWrite容器即写时复制的容器。通俗的理解是当我们往一个容器添加元素的时候,不直接往当前容器添加,而是先将当前容器进行Copy,复制出一个新的容器,然后新的容器里添加元素,添加完元素之后,再将原容器的引用指向新的容器。这样做的好处是我们可以对CopyOnWrite容器进行并发的读,而不需要加锁,因为当前容器不会添加任何元素。所以CopyOnWrite容器也是一种读写分离的思想,读和写不同的容器。

CopyOnWriteArrayList的实现原理

在使用CopyOnWriteArrayList之前,我们先阅读其源码了解下它是如何实现的。以下代码是向ArrayList里添加元素,可以发现在添加的时候是需要加锁的,否则多线程写的时候会Copy出N个副本出来。

public boolean add(T e) {
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
        Object[] elements = getArray();
        int len = elements.length;
        // 复制出新数组
        Object[] newElements = Arrays.copyOf(elements, len + 1);
        // 把新元素添加到新数组里
        newElements[len] = e;
        // 把原数组引用指向新数组
        setArray(newElements);
        return true;
    } finally {
        lock.unlock();
    }
}

final void setArray(Object[] a) {
    array = a;
}

读的时候不需要加锁,如果读的时候有多个线程正在向ArrayList添加数据,读还是会读到旧的数据,因为写的时候不会锁住旧的ArrayList。

public E get(int index) {
    return get(getArray(), index);
}

JDK中并没有提供CopyOnWriteMap,我们可以参考CopyOnWriteArrayList来实现一个,基本代码如下:

import java.util.Collection;
import java.util.Map;
import java.util.Set;
public class CopyOnWriteMap<K, V> implements Map<K, V>, Cloneable {
    private volatile Map<K, V> internalMap;
    
    public CopyOnWriteMap() {
        internalMap = new HashMap<K, V>();
    }
    
    public V put(K key, V value) {
        synchronized (this) {
            Map<K, V> newMap = new HashMap<K, V>(internalMap);
            V val = newMap.put(key, value);
            internalMap = newMap;
            return val;
        }
    }
    
    public V get(Object key) {
        return internalMap.get(key);
    }
    
    public void putAll(Map<? extends K, ? extends V> newData) {
        synchronized (this) {
            Map<K, V> newMap = new HashMap<K, V>(internalMap);
            newMap.putAll(newData);
            internalMap = newMap;
        }
    }
}

实现很简单,只要了解了CopyOnWrite机制,我们可以实现各种CopyOnWrite容器,并且在不同的应用场景中使用。

CopyOnWrite的应用场景

CopyOnWrite并发容器用于读多写少的并发场景。比如白名单,黑名单,商品类目的访问和更新场景,假如我们有一个搜索网站,用户在这个网站的搜索框中,输入关键字搜索内容,但是某些关键字不允许被搜索。这些不能被搜索的关键字会被放在一个黑名单当中,黑名单每天晚上更新一次。当用户搜索时,会检查当前关键字在不在黑名单当中,如果在,则提示不能搜索。实现代码如下:

public class BlackListServiceImpl {
    private static CopyOnWriteMap<String, Boolean> blackListMap = 
    new CopyOnWriteMap<String, Boolean>( 1000);
            
    public static boolean isBlackList(String id) {
        return blackListMap.get(id) == null ? false : true;
    }
    
    public static void addBlackList(String id) {
        blackListMap.put(id, Boolean.TRUE);
    }
    
    /**
     * 批量添加黑名单
     *
     * @param ids
     */
    public static void addBlackList(Map<String,Boolean> ids) {
        blackListMap.putAll(ids);
    }
}

代码很简单,但是使用CopyOnWriteMap需要注意两件事情:

  1. 减少扩容开销。根据实际需要,初始化CopyOnWriteMap的大小,避免写时CopyOnWriteMap扩容的开销。

  2. 使用批量添加。因为每次添加,容器每次都会进行复制,所以减少添加次数,可以减少容器的复制次数。如使用上面代码里的addBlackList方法。

CopyOnWrite的缺点

CopyOnWrite容器有很多优点,但是同时也存在两个问题,即内存占用问题和数据一致性问题。所以在开发的时候需要注意一下。

内存占用问题

因为CopyOnWrite的写时复制机制,所以在进行写操作的时候,内存里会同时驻扎两个对象的内存,旧的对象和新写入的对象(注意:在复制的时候只是复制容器里的引用,只是在写的时候会创建新对象添加到新容器里,而旧容器的对象还在使用,所以有两份对象内存)。如果这些对象占用的内存比较大,比如说200M左右,那么再写入100M数据进去,内存就会占用300M,那么这个时候很有可能造成频繁的Yong GC和Full GC。之前我们系统中使用了一个服务由于每晚使用CopyOnWrite机制更新大对象,造成了每晚15秒的Full GC,应用响应时间也随之变长。

针对内存占用问题,可以通过压缩容器中的元素的方法来减少大对象的内存消耗,比如,如果元素全是10进制的数字,可以考虑把它压缩成36进制或64进制。或者不使用CopyOnWrite容器,而使用其他的并发容器,如ConcurrentHashMap

数据一致性问题

CopyOnWrite容器只能保证数据的最终一致性,不能保证数据的实时一致性。所以如果你希望写入的的数据,马上能读到,请不要使用CopyOnWrite容器。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

java并发编程:CopyOnWrite容器介绍 的相关文章

  • 添加动态数量的监听器(Spring JMS)

    我需要添加多个侦听器 如中所述application properties文件 就像下面这样 InTopics Sample QUT4 Sample T05 Sample T01 Sample JT7 注意 这个数字可以多一些 也可以少一些
  • 对话框上的 EditText 不返回任何文本

    我太累了 找不到错误 我没有发现任何错误 但我没有从 editText 收到任何文本 请看下面的代码 活动密码 xml
  • Grails 2.3.0 自动重新加载不起作用

    我最近将我们的项目升级到 grails 2 3 0 一切工作正常 除了每当我更改代码时自动重新加载都无法工作的问题 这包括所有项目工件 控制器 域 服务 gsps css 和 javascript 文件 我的旧版本 grails 可以正常工
  • Spring安全“记住我”cookie在第一个请求中不可用

    我无法在登录请求后检索 Spring 记住我 cookie 但它在对受保护页面的下一个请求中工作正常 谁能告诉我怎样才能立即得到它 我在登录请求中设置了记住我的 cookie 但在 Spring 重定向回原始 受保护的 url 后无法检索它
  • Condition 接口中的 signalAll 与对象中的 notificationAll

    1 昨天我才问过这个问题条件与等待通知机制 https stackoverflow com questions 10395571 condition vs wait notify mechanism 2 我想编辑相同的内容并在我的问题中添加
  • 主线程如何在该线程之前运行?

    我有以下代码 public class Derived implements Runnable private int num public synchronized void setA int num try Thread sleep 1
  • 记录骆驼路线

    我的项目中有几个 Camel 上下文 如果可能的话 我想以逆向工程方式记录路线 因为我们希望保持与上下文相关的文档最新 最好的方法是什么 我们倾向于预先实际设计路线 并使用来自EIP book http www eaipatterns co
  • Java 服务器-客户端 readLine() 方法

    我有一个客户端类和一个服务器类 如果客户端向服务器发送消息 服务器会将响应发送回客户端 然后客户端将打印它收到的所有消息 例如 如果客户端向服务器发送 A 则服务器将向客户端发送响应 1111 所以我在客户端类中使用 readLine 从服
  • Java 8 中函数式接口的使用

    这是来自的后续问题Java 8 中的 双冒号 运算符 https stackoverflow com questions 20001427 double colon operator in java 8其中 Java 允许您使用以下方式引用
  • Java 数组的最大维数

    出于好奇 在 Java 中数组可以有多少维 爪哇language不限制维数 但是JavaVM规范将维度数限制为 255 例如 以下代码将无法编译 class Main public static void main String args
  • 在 Spring Boot Actuator 健康检查 API 中启用日志记录

    我正在使用 Spring boot Actuator APIproject https imobilenumbertracker com 拥有一个健康检查端点 并通过以下方式启用它 management endpoints web base
  • Dispatcher-servlet 无法映射到 websocket 请求

    我正在开发一个以Spring为主要框架的Java web应用程序 特别使用Spring core Spring mvc Spring security Spring data Spring websocket 像这样在 Spring 上下文
  • 逃离的正确方法是什么?使用 Oracle 12c MATCH_RECOGNIZE 时 JDBCPreparedStatement 中的字符?

    以下查询在 Oracle 12c 中是正确的 SELECT FROM dual MATCH RECOGNIZE MEASURES a dummy AS dummy PATTERN a DEFINE a AS 1 1 但它不能通过 JDBC
  • 尝试使用等于“是”或“否”的字符串变量重新启动 do-while 循环

    计算行程距离的非常简单的程序 一周前刚刚开始 我有这个循环用于解决真或假问题 但我希望它适用于简单的 是 或 否 我为此分配的字符串是答案 public class Main public static void main String a
  • 挂钩 Eclipse 构建过程吗?

    我希望在 Eclipse 中按下构建按钮时能够运行一个简单的 Java 程序 目前 当我单击 构建 时 它会运行一些 JRebel 日志记录代码 我有一个程序可以解析 JRebel 日志文件并将统计信息存储在数据库中 是否可以编写一个插件或
  • Android - 9 补丁

    我正在尝试使用 9 块图片创建一个新的微调器背景 我尝试了很多方法来获得完美的图像 但都失败了 s Here is my 9 patch 当我用Draw 9 patch模拟时 内容看起来不错 但是带有箭头的部分没有显示 或者当它显示时 这部
  • Java &= 运算符应用 & 或 && 吗?

    Assuming boolean a false 我想知道是否这样做 a b 相当于 a a b logical AND a is false hence b is not evaluated 或者另一方面 这意味着 a a b Bitwi
  • JAXB - 列表<可序列化>?

    我使用 xjc 制作了一些课程 public class MyType XmlElementRefs XmlElementRef name MyInnerType type JAXBElement class required false
  • 在哪里存储 Java 的 .properties 文件?

    The Java教程 http download oracle com javase tutorial essential environment properties htmlon using Properties 讨论如何使用 Prop
  • Android 和 Java 中绘制椭圆的区别

    在Java中由于某种原因Ellipse2D Double使用参数 height width x y 当我创建一个RectF在Android中参数是 left top right bottom 所以我对适应差异有点困惑 如果在 Java 中创

随机推荐

  • 火力全开!华为云发布三大新品,提供一站式数据库智能服务

    摘要 当传统数据库搭上智能的便车 会怎样 为期三天的 2018 华为全联接大会已经圆满结束 但惊喜还在继续 智能 见未来 当印象中机房一排排摆满设备的传统数据库搭上智能的便车 又会演绎出一幅怎样的画卷 不妨看看本届 HC 大会上华为云数据库
  • 新浪微博 [异常问题] 414 Request-URL Too Large

    新浪微博 异常问题 414 Request URL Too Large 浏览器上打开新浪微博 或则日志是返回结果提示 414 Request URL Too Large原因 因同IP访问微博页面过多 IP被微博限制访问解决方法 1 更改本机
  • servlet实现的三种方式

    摘要 本次讲解的内容是关于Servlet实现的三种方式 1 通过继承Servlet接口来实现构造自己的Servlet类 由于提供的Servlet是一个接口 所以我们需要重写 覆盖 Servlet接口的所有方法 代码实现 servlet方法的
  • python图像分析_Python图像处理

    作者 Garima Singh 编译 VK 来源 Git Connected 以前照相从来没有那么容易 现在你只需要一部手机 拍照是免费的 如果我们不考虑手机的费用的话 就在上一代人之前 业余艺术家和真正的艺术家如果拍照非常昂贵 并且每张照
  • MySQL-内连接、外连接和全连接

    连接过程 连接过程首先要确定第一个表 称为驱动表 驱动表上查询的每一条记录分别需要到被驱动表上查找符合过滤条件的记录 因此驱动表只需要访问一次 被驱动表可能需要访问多次 内连接 对于内连接的两个表 驱动表中的记录在被驱动表中找不到匹配的记录
  • php echo换行

  • 前端终于要破局了!

    正文 前段时间 掘金热帖 放心 前端死不了 在前端圈疯传 百度前端大佬表明 前端技术是依托于互联网行业的 只要行业还在 它就会有用武之地 就会有价值 总的来说 技能跟上发展 前端就不会死 谁掌握得更深 应用得更好 谁就更容易脱颖而出 为此
  • Spark报Total size of serialized results of 12189 tasks is bigger than spark.driver.maxResultSize

    一 异常信息 Total size of serialized results of 12189 tasks is bigger than spark driver maxResultSize 1024M Total size of ser
  • [万能解决问题]MATLAB has encountered an internal problem and needs to close.

    1 错误的描述及解决办法 使用Matlab和C 混合编程时 即编写完mex文件 调用时 经常会提示下面的错误 触发上述错误的情况 1 如果一进入mexFunction函数就报错 即不会命中函数中设置的任何断点 也会报错 那么说明 你忘记了将
  • Keil5的仿真调试

    Keil5基本的仿真调试操作 首先点击魔法棒 然后输入你板子上所用的晶振 然后进入debug 然后选择 Use Simulator 然后点击OK 然后点击调试按钮 然后就会出现调试页面 我这里是已经把汇编窗口给挪到右侧了 你第一次打开可能是
  • 红黑树详解

    1 红黑树的概念 红黑树 是一种二叉搜索树 但在每个结点上增加一个存储位表示结点的颜色 可以是Red或 Black 通过对任何一条从根到叶子的路径上各个结点着色方式的限制 红黑树确保没有一条路径会比其他路径长出俩倍 因而是接近平衡的 红黑树
  • 【C】数组的地址

    目录 一维数组 二维数组 数组的地址和数组首元素地址相同 只有在sizeof 和 的情况下 取出的是整个数组的地址 其他情况下都是首元素地址 一维数组 sizeof 有 无 二维数组 sizeof 有 无 注意 这里二维数组的一个元素中是两
  • Docker高级——网络配置

    Docker网络 默认网络 安装 Docker 以后 会默认创建三种网络 可以通过 docker network ls 查看 root test docker network ls NETWORK ID NAME DRIVER SCOPE
  • html自定义列表第三层嵌套,搜索引擎优化一般不抓取三层以上的表格嵌套

    一 DIV CSS的网页制作对 SEO 的优势 由于国外都流行用DIV CSS来制作网页 这点与大多国内的企业站点用TABLE不同 所以想谈一下DIV CSS的网页制作对 SEO 的优势有哪些 这些都是比较于TABLE而言的 DIV CSS
  • Python爬虫实战:2020最新京东商品数据爬虫保姆式教程(小白也能懂)!

    Python爬虫 基于Scrapy爬取京东商品数据并保存到mysql且下载图片 一 项目准备 二 网页及代码分析 三 完整代码 一 项目准备 创建scrapy京东项目 scrapy startproject Jingdong cd Jing
  • QT setWindowFlags函数

    Qt Widget 是一个窗口或部件 有父窗口就是部件 没有就是窗口 Qt Window 是一个窗口 有窗口边框和标题 Qt Dialog 是一个对话框窗口 Qt Sheet 是一个窗口或部件Macintosh表单 Qt Drawer 是一
  • 小程序 function(res)与(res) =>的区别

    前者不可使用 this setData cameraImg res tempImagePath 后者可以使用 this setData cameraImg res tempImagePath 如果在一个对象的方法里面
  • acadacad经典工作空间.cuix_自定义还原CAD“经典”绘图空间?(适用2021~~2015版本)...

    你已选中了添加链接的内容 本文由小8整理首发 版权归本公众号所有 如有侵 请自行删除 在位编辑器 软件安装 应用技巧 图库资料 视频教程 很多小伙伴经常问怎样切换 经典空间 的问题了 从CAD2015版本开始 官方已经取消 AutoCAD
  • response.getWriter().write()和 response.getWriter().print()的区别 以及 PrintWriter对象 和 out对象 的区别

    感谢原文作者 krismile qh 原文链接 https blog csdn net krismile qh article details 89926001 一 response getWriter write 和 response g
  • java并发编程:CopyOnWrite容器介绍

    前言 Copy On Write简称COW 是一种用于程序设计中的优化策略 其基本思路是 从一开始大家都在共享同一个内容 当某个人想要修改这个内容的时候 才会真正把内容Copy出去形成一个新的内容然后再改 这是一种延时懒惰策略 从JDK1