设计模式学习笔记 - 设计模式与范式 - 创建型:7.原型模式:如何快速地clone一个HashMap散列表

原型模式的原理与应用

如果对象的创建成本比较大,而同一个类的不同对象之间差别不大(大部分字段都相同),在这种情况下,我们可以利用对已有对象(原型)进行复制(或者叫拷贝)的方式来创建新对象,以达到节省创建时间的目的。这种基于原型来创建对象的方式就叫做原型设计模式(prototype Design Pattern),简称原型模式

那何为“对象的创建成本比较大”?

实际上,创建对象包含的申请内容、给成员变量赋值这一过程,本身并不会花费太多时间。应用一个复杂的模式,只得到一点点的性能提升,这就是所谓的过度设计,得不偿失。

但是如果对象中的数据需要经过复杂的计算才能得到(比如排序、计算哈希值),或者从 RPC、网络、数据库、文件系统等非常慢速的 IO 中读取,这种情况下,我们就可以利用原型模式,从其他已有对象中直接拷贝得到,而不用每次在创建新对象的时候,都重复执行这些耗时的操作。

这么说还是比较理论,通过一个例子来解释下刚刚的这段话

假设数据库中存储了大约 10 万条 “搜索关键词” 信息,每条信息包含关键词、关键词被搜索的次数、信息最近被更新的时间等。系统 A 在启动时会加载这份数据到内存中,用于处理某些其他的业务需求。为了方便快速地查找某个关键词的信息,我们给关键词建立一个散列表索引(Java 中的 HashMap)。

不过,我们还有另外一个系统 B,专门用来分析搜索日志,定期(比如间隔 10 分钟)批量地更新数据库中的数据,并且标记为新的数据版本。比如,在下面的示例图中,我们对 V2 版本的数据进行更新,得到 V3 版本的数据。这里我们假设只有更新和新增关键词,没有删除关键词的行为。

在这里插入图片描述

为保证系统 A 中数据的实时性(不一定非常实时,但数据也不能太旧),系统 A 需要定期根据数据库中的数据,更新内存中的索引和数据。

我们该如何实现这个需求呢?

实际上,也不难。我们只需要在系统 A 中,记录当前数据的版本 Va 对应的更新时间 Ta,从数据库中捞出更新时间大于 Ta 的所有搜索关键词,也就是找出 Va 本班与最新版本数据的 “差集”,然后针对差集中的每个关键词进行处理。

  • 如果它已经在散列表中存在了,我们就更新相应的搜索次数、更新时间等信息;
  • 如果它在散列表中不存在,我们就将它插入到散列表中。

按照这个设计思路,给出的示例代码如下所示:

public class Demo {
    private ConcurrentHashMap<String, SearchWord> currentKeywords = new ConcurrentHashMap<>();
    private long lastUpdateTime = -1;

    public void refresh() {
        // 从数据库中取出更新时间>lastUpdateTime的数据,放入到currentKeywords
        List<SearchWord> toBeUpdatedSearchWords = getSearchWords(lastUpdateTime);
        long maxNewUpdatedTime = lastUpdateTime;
        for (SearchWord searchWord : toBeUpdatedSearchWords) {
            if (searchWord.getLastUpdateTime() > maxNewUpdatedTime) {
                maxNewUpdatedTime = searchWord.getLastUpdateTime();
            }
            if (currentKeywords.contains(searchWord.getKeyWord())) {
                currentKeywords.replace(searchWord.getKeyWord(), searchWord);
            } else {
                currentKeywords.put(searchWord.getKeyWord(), searchWord);
            }
        }
        lastUpdateTime = maxNewUpdatedTime;
    }

    private List<SearchWord> getSearchWords(long lastUpdateTime) {
        // 从数据库中取出更新时间>lastUpdateTime的数据
        return null;
    }
}

不过,现在,有一个特殊的要求:任何时刻,系统 A 中的所有数据都必须是同一个版本的,要么是版本 a,要么是版本 b,不能有的是版本 a,有的是版本 b。那么,刚刚的更新方式就不能满足这个要求了。此外,还要求:在更新内存数据的时候,系统 A 不能处于不可用状态,也就是不能停机更新数据。

实际上,也不难。我们把正在使用的数据版本定义为 “服务版本”,当我们要更新内存中的数据时,并不是直接在服务版本(版本 a)上更新,而是重新创建另一个版本数据(版本 b),等新的版本数据建好之后,再一次性地将服务版本从版本 a 切换到版本 b。这样既保证了数据一直可用,又避免了中间状态的存在。

按照这个设计思路,给出的示例代码如下:

public class Demo {
    private HashMap<String, SearchWord> currentKeywords = new HashMap<>();

    public void refresh() {
        HashMap<String, SearchWord> newKeywords = new LinkedHashMap<>();

        // 从数据库中取出所有数据,放入到newKeywords
        List<SearchWord> toBeUpdatedSearchWords = getSearchWords();
        for (SearchWord searchWord : toBeUpdatedSearchWords) {
            newKeywords.put(searchWord.getKeyWord(), searchWord);
        }
        currentKeywords = newKeywords;
    }

    private List<SearchWord> getSearchWords() {
        // 从数据库中取出所有数据
        return null;
    }
}

不过,在上面的代码实现中,newKeywords 构建的成本比较高。我们需要将这 10 万条数据从数据库中读出,然后计算哈希值,构建 newKeywords。这个过程显然是比较耗时的。为了提高效率,原型模式就派上用场了。

我们拷贝 currentKeywords 数据到 newKeywords 中,然后从数据库中只捞出新增或有更新的关键词,更新到 newKeywords 中。而相对于 10 万条数据来说,每次新增或者更新的关键词个数是比较少的,所以,这种策略大大提高了数据更新的效率。

按照这个设计思路,给出的示例代码如下:

public class Demo {
    private HashMap<String, SearchWord> currentKeywords = new HashMap<>();
    private long lastUpdateTime = -1;

    public void refresh() {
        // 原型模式就是这么简单,拷贝已有对象的数据,更新少量差值
        HashMap<String, SearchWord> newKeywords = (HashMap<String, SearchWord>) currentKeywords.clone();

        // 从数据库中取出更新时间>lastUpdateTime的数据,放入到newKeywords
        List<SearchWord> toBeUpdatedSearchWords = getSearchWords(lastUpdateTime);
        long maxNewUpdatedTime = lastUpdateTime;
        for (SearchWord searchWord : toBeUpdatedSearchWords) {
            if (searchWord.getLastUpdateTime() > maxNewUpdatedTime) {
                maxNewUpdatedTime = searchWord.getLastUpdateTime();
            }
            if (newKeywords.containsKey(searchWord.getKeyWord())) {
                SearchWord oldSearchWord = newKeywords.get(searchWord.getKeyWord());
                oldSearchWord.setCount(searchWord.getCount());
            } else {
                newKeywords.put(searchWord.getKeyWord(), searchWord);
            }
        }
        lastUpdateTime = maxNewUpdatedTime;
        currentKeywords = newKeywords;
    }

    private List<SearchWord> getSearchWords(long lastUpdateTime) {
        // 从数据库中取出更新时间>lastUpdateTime的数据
        return null;
    }
}

这里,我们利用了 Java 中的 clone() 来复制一个对象。不知道你有没有发现,实际上刚刚的代码实现是有问题的。要弄明白到底有什么问题,我们需要先了解两个概念:深拷贝(Deep Copy)和浅拷贝(Shallow Copy)。

原型的实现方式:深拷贝和浅拷贝

在内存中,用散列表组织的搜索表组织的搜索关键词信息是如何存储的。大致结构如下所示。从图中我们可以发现,散列表索引中,每个结点存储的 key 是搜索关键词,value 是 SearchWord 对象的内存地址。SearchWord 对象本身存储在散列表之外的内存空间中。

在这里插入图片描述

浅拷贝和深拷贝的区别在于,浅拷贝只会复制图中的索引(散列表),不会复制数据本身。浅拷贝得到的对象(newKeyWords)跟原始对象(currentKeywords)共享数据(SearchWord 对象),而深拷贝得到的是一份完完全全独立的对象。具体的对比如下图所示:

在这里插入图片描述
在这里插入图片描述
在 Java 语言中,Object 类的 clone() 方法执行的就是浅拷贝。它只会拷贝对象中的基本数据类型的数据(int、long),以及引用对象(SearchWord)的内存地址,不会递归地拷贝引用对象本身。

在上面的代码中,我们通过调用 HashMap 上 clone() 浅拷贝方式来实现原型模式。当我们通过 newKeywords 更新 currentKeywords 对象的时候,newKeywordscurrentKeywords 因为指向相同的一组 SearchWord 对象,就会导致 currentKeywords 中执行的 SearchWord,有的是指老版本的,有的是新版本的,就没法满足我们之前的需求:currentKeywords 中的数据在任何时刻都是同一个版本的,不存在介于老版本与新版本之间的中间状态。

可以将浅拷贝替换为深拷贝。newKeywords 不仅仅复制 currentKeywords 中的索引,还把 SearchWord 对象也复制一份出来,这样 newKeywordscurrentKeywords 就指向不同的 SearchWord 对象,也就不存在更新 newKeywords 的数据就会导致 currentKeywords 的数据也被更新的问题了。

如何实现深拷贝呢? 有下面两种方式

  • 第一种方法:递归拷贝对象,对象的引用对象以及引用对象的引用对象…直到要拷贝的对象只包含基本数据类型,没有引用对象位置。根据这个思路对之前的代码进行重构。重构之后的代码如下所示:
public class Demo {
    private HashMap<String, SearchWord> currentKeywords = new HashMap<>();
    private long lastUpdateTime = -1;

    public void refresh() {
        // Deap copy
        HashMap<String, SearchWord> newKeywords = new HashMap<>();
        for (Map.Entry<String, SearchWord> e : currentKeywords.entrySet()) {
            SearchWord searchWord = e.getValue();
            SearchWord newSearchWord = new SearchWord(
                    searchWord.getKeyWord(), searchWord.getCount(), searchWord.getLastUpdateTime());
            newKeywords.put(e.getKey(), newSearchWord);
        }
        
        // 从数据库中取出更新时间>lastUpdateTime的数据,放入到newKeywords
        List<SearchWord> toBeUpdatedSearchWords = getSearchWords(lastUpdateTime);
        long maxNewUpdatedTime = lastUpdateTime;
        for (SearchWord searchWord : toBeUpdatedSearchWords) {
            if (searchWord.getLastUpdateTime() > maxNewUpdatedTime) {
                maxNewUpdatedTime = searchWord.getLastUpdateTime();
            }
            if (newKeywords.containsKey(searchWord.getKeyWord())) {
                SearchWord oldSearchWord = newKeywords.get(searchWord.getKeyWord());
                oldSearchWord.setCount(searchWord.getCount());
            } else {
                newKeywords.put(searchWord.getKeyWord(), searchWord);
            }
        }
        lastUpdateTime = maxNewUpdatedTime;
        currentKeywords = newKeywords;
    }

    private List<SearchWord> getSearchWords(long lastUpdateTime) {
        // 从数据库中取出更新时间>lastUpdateTime的数据
        return null;
    }
}
  • 第二种方法:先将对象序列化,然后再反序列化成新的对象。具体的示例代码如下所示:
    public Object deepCopy(Object object) {
        ByteArrayOutputStream bo = new ByteArrayOutputStream();
        ObjectOutputStream oo = new ObjectOutputStream(bo);
        oo.writeObject(object);

        ByteArrayInputStream bi = new ByteArrayInputStream(bo.toByteArray());
        ObjectInputStream oi = new ObjectInputStream(bi);
        return oi.readObject();
    }

刚刚的两种实现方式,不管采用哪种,深拷贝都要比浅拷贝耗时、耗费内存空间。针对我们这个应用场景,有没有更快、更省内存的实现方式呢?

可以先采用浅拷贝的方式创建 newKeywords。对于需要更新的 SearchWord 对象,我们在使用深拷贝的方式创建一份新的对象,替换 newKeywords 中的老对象(与系统的写时复制技术 Copy-On-Write,比较类似)。毕竟需要更新的数据时很少的。这种方式既利用了浅拷贝节省时间、空间的优点,又能保证 currentKeywords 中的数据都是老版本的数据。具体的实现代码如下所示。这也是标题中讲的,在这个应用场景下,最快速 clone 散列表的方式

public class Demo {
    private HashMap<String, SearchWord> currentKeywords = new HashMap<>();
    private long lastUpdateTime = -1;

    public void refresh() {
        // Shallow copy
        HashMap<String, SearchWord> newKeywords = (HashMap<String, SearchWord>) currentKeywords.clone();
        // 从数据库中取出更新时间>lastUpdateTime的数据,放入到newKeywords
        List<SearchWord> toBeUpdatedSearchWords = getSearchWords(lastUpdateTime);
        long maxNewUpdatedTime = lastUpdateTime;
        for (SearchWord searchWord : toBeUpdatedSearchWords) {
            if (searchWord.getLastUpdateTime() > maxNewUpdatedTime) {
                maxNewUpdatedTime = searchWord.getLastUpdateTime();
            }
            if (newKeywords.containsKey(searchWord.getKeyWord())) {
                newKeywords.remove(searchWord.getKeyWord());
            }
            newKeywords.put(searchWord.getKeyWord(), searchWord);
        }
        lastUpdateTime = maxNewUpdatedTime;
        currentKeywords = newKeywords;
    }

    private List<SearchWord> getSearchWords(long lastUpdateTime) {
        // 从数据库中取出更新时间>lastUpdateTime的数据
        return null;
    }
}

总结

1.什么是原型模式

如果对象的创建成本比较大,而同一个类的对象之间差别不大(大部分字段都相同),在这种情况下,我们可以利用已有对象(原型)就进行复制(或者叫拷贝)的方式,来创建新对象,已达到节省创建时间的目的。这种基于原型来创建对象的方式就叫做原型设计模式,简称原型模式。

2.原型模式的两种实现方法

原型模式有两种实现方式,深拷贝和浅拷贝。

  • 浅拷贝只会复制对象中基本数据类型数据和引用对象的内存地址,不会递归地复制引用对象,以及引用对象的引用对象…
  • 深拷贝得到的是一份完完全全独立的对象。

所以深拷贝比浅拷贝更加耗时,更加耗内存空间。

如果要拷贝的对象是不可变对象,浅拷贝共享不可变对象是没有问题的,但对于可变对象来说,浅拷贝得到的对象和原始对象会共享部分数据,就有可能出现数据被修改的风险,也就变得复杂多了。除非像我们今天实战中举的那个里子,需要从数据库中加载 10 万条数据并构建散列表索引,操作非常耗时,这种情况下比较推荐使用浅拷贝,否则,没有充分的理由,不要为了一点点的性能提升而使用浅拷贝

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/484211.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Lunule: An Agile and Judicious Metadata Load Balancer for CephFS——论文阅读

SC 2021 Paper 分布式元数据论文阅读笔记 问题 CephFS采用动态子树分区方法&#xff0c;将分层命名空间划分并将子树分布到多个元数据服务器上。然而&#xff0c;这种方法存在严重的不平衡问题&#xff0c;由于其不准确的不平衡预测、对工作负载特性的忽视以及不必要/无效的迁…

解码新时代内存架构:探秘数据在内存中的灵动驻足

欢迎来到白刘的领域 Miracle_86.-CSDN博客 系列专栏 C语言知识 先赞后看&#xff0c;已成习惯 创作不易&#xff0c;多多支持&#xff01; 随着信息技术的飞速发展&#xff0c;我们身处一个数据爆炸的时代。数据的处理和存储方式正日益成为技术革新的重要领域。在新时代的…

【Java】高级篇2:多线程

一、相关概念 注意&#xff1a; 1、不同进程之间不共享内存 2、进程之间的数据交换和通信成本很高 线程调度&#xff1a; 单核CPU与多核CPU&#xff1a; 并行与并发&#xff1a; 二、创建和启动线程 1、概述 2、方式 2.1 方式一&#xff1a;继承Thread类 2.2 方式二&#xf…

Fantasy RPG Spell Pack 2

介绍奇幻角色扮演游戏魔法包VFX,这是为您的Unity奇幻角色扮演游戏提供的终极视觉效果解决方案!这个包包含30个独特的VFX,将为您的法术和能力带来生命,让您的玩家沉浸在魔法和奇迹的世界中。 从令人惊叹的彩虹盾和闪电到旋转门户和召唤圈,这个包有你需要的一切来创造一个真…

GETSHELL方法总结上

渗透的总步骤 1.信息收集找到弱漏洞 2.漏洞挖掘 漏洞验证 3.有一定权限 getshell 4.提权后---渗透 5.内网渗透】 前后台拿shell方法汇总 接下来我们实操一波dedecms也就是织梦cms 如果你们的靶场是空白的 可能是php版本 我们修改为5.2 可能是源码问题 我们不要急着上传…

ChatGPT论文指南|揭秘8大ChatGPT提示词研究技巧提升写作效率【建议收藏】

点击下方▼▼▼▼链接直达AIPaperPass &#xff01; AIPaperPass - AI论文写作指导平台 公众号原文▼▼▼▼&#xff1a; ChatGPT论文指南|揭秘8大ChatGPT提示词研究技巧提升写作效率【建议收藏】 目录 1.写作方法 2.方法设计 3.研究结果 4.讨论写作 5.总结结论 6.书…

MySQL--select count(*)、count(1)、count(列名) 的区别你知道吗?

MySQL select count(*)、count(1)、count(列名) 的区别&#xff1f; 这里我们先给出正确结论&#xff1a; count(*)&#xff0c;包含了所有的列&#xff0c;会计算所有的行数&#xff0c;在统计结果时候&#xff0c;不会忽略列值为空的情况。count(1)&#xff0c;忽略所有的列…

Axure RP 9 for mac中文版密钥激活版下载

Axure RP 9是一款专业的快速原型设计工具&#xff0c;它可以帮助产品设计师、交互设计师和用户体验设计师等创建高保真度、交互性强的原型&#xff0c;以便在产品开发之前进行测试和用户验证。 软件下载&#xff1a;Axure RP 9 for mac中文版密钥激活版下载 该工具具有丰富的功…

2023蓝桥杯C/C++A组省赛 B题: 有奖问答|DFS搜索 、线性dp

题目链接&#xff1a; 1.有奖问答 - 蓝桥云课 (lanqiao.cn) 说明&#xff1a; DFS做法&#xff1a; 因为是填空题&#xff0c;不用考虑超时&#xff0c;首先先考虑暴力做法DFS来做&#xff0c;根据题意&#xff0c;30道题&#xff0c;有一个答题的先后顺序&#xff0c;上一…

【算法篇】逐步理解动态规划1(斐波那契数列模型)

目录 斐波那契数列模型 1. 第N个泰波那契数 2.使用最小花费爬楼梯 3.解码方法 学过算法的应该知道&#xff0c;动态规划一直都是一个非常难的模块&#xff0c;无论是状态转移方程的定义还是dp表的填表&#xff0c;都非常难找到思路。在这个算法的支线专题中我会结合很多力…

Java学习笔记 | Java基础语法 | 03 | 流程控制语句

文章目录 0 前言1.流程控制语句1.1 流程控制语句分类1.2 顺序结构 2.判断语句2.1 if语句1. if语句格式1练习1&#xff1a;老丈人选女婿练习2&#xff1a;考试奖励 2. if语句格式2练习1&#xff1a;吃饭练习2&#xff1a;影院选座 3. if语句格式3练习1&#xff1a;考试奖励 2.2 …

Vue使用font-face自定义字体详解

目录 1 介绍2 使用2.1 语法2.2 属性说明2.3 Vue使用案例2.3.1 全局定义字体2.3.2 在页面使用 3 注意事项 1 介绍 font-face 是 CSS 中的一个规则&#xff0c;它允许你加载服务器上的字体文件&#xff08;远程或者本地&#xff09;&#xff0c;并在网页中使用这些字体。这样&am…

使用 STL 容器发生异常的常见原因分析与总结

目录 1、概述 2、使用STL列表中的元素越界 3、遍历STL列表删除元素时对迭代器自加处理有问题引发越界 4、更隐蔽的遍历STL列表删除元素时引发越界的场景 5、多线程同时操作STL列表时没有加锁导致冲突 6、对包含STL列表对象的结构体进行memset操作导致STL列表对象内存出异…

大学教材《C语言程序设计》(浙大版)课后习题解析 | 第一、二章

概述 本文主要提供《C语言程序设计》(浙大版) 第一、二章课后习题解析&#xff0c;以方便同学们完成题目后作为参考对照。后续将写出三、四章节课后习题解析&#xff0c;如想了解更多&#xff0c;请持续关注该专栏。 专栏直达链接&#xff1a;《C语言程序设计》(浙大版)_孟俊宇…

Hive SQL必刷练习题:复购率问题(*****)

是说这个数据表中&#xff0c;找到最后一天 &#xff0c;也就是今天的日期&#xff0c;max(date) over()S today 【借助开窗函数】 截至最后一天位置&#xff0c;也就是“今天“&#xff0c;表中的最新的一天 去看90天内“某商品复购率 近90天内购买它至少两次的人数 购买它…

c++常考基础知识(2)

二.c关键字 关键字汇总 c中共有63个关键字&#xff0c;其中包括int&#xff0c;char&#xff0c;double等类型关键字&#xff0c;if&#xff0c;else&#xff0c;while&#xff0c;do&#xff0c;等语法关键字&#xff0c;还有sizeof等函数关键字。 三.数据结构 1.数组&#x…

常见的OOM 问题的 6 种场景

今天跟大家一起聊聊线上服务出现 OOM 问题的 6 种场景,希望对你会有所帮助。 一、堆内存 OOM 堆内存 OOM 是最常见的 OOM 了。 出现堆内存 OOM 问题的异常信息如下: java.lang.OutOfMemoryError: Java heap space此 OOM 是由于 JVM 中 heap 的最大值,已经不能满足需求了…

Git的原理和使用(四)

目录 远程操作 理解分布式版本控制系统 远程仓库 新建远程仓库 克隆远程仓库 向远程仓库推送 拉取远程仓库 配置Git 忽略特殊文件 为命令配置别名 标签管理 理解标签 创建标签 操作标签 远程操作 理解分布式版本控制系统 1、每个人的电脑上都是一个完整的版本库…

批量重命名文件名,批量管理文件,复制后轻松删除原文件

在现代工作中&#xff0c;我们经常需要处理大量的文件&#xff0c;无论是工作文档、图片还是视频资料。对于很多人来说&#xff0c;文件管理是一项繁琐且耗时的任务。不过&#xff0c;现在有一种高效的文件管理方法&#xff0c;可以帮助你轻松复制文件后删除原文件夹&#xff0…

2024.03.24 exam

2024.03.24 exam 据说是事业单位考试例题&#xff0c;娱乐一下脑子