设计模式-原型模式

设计模式-原型模式

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

案例分析

由于某种原因,需要在内存中维护一个 hashMap,存储附件对应的 MD5 值和附件 ID,用来统计某个附件是否已经上传过,或者说是否重复,MD5 值业务数据并不需要,所以数据库中没有实际字段。由于并发量很大,如果在上传附件的时候同步计算 MD5 值并利用锁控制存入 hashMap 会影响请求时的响应速度,且该功能只作为统计,因此要求使用后台的定时任务实现。

目前为止其实功能很简单,就是启动任务定时更新 hashMap 里的内容

java 复制代码
package com.xsdl.prototype;

import cn.hutool.crypto.digest.MD5;
import com.xsdl.prototype.pojo.File;

import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.stream.Collectors;
import java.util.stream.IntStream;

public class Main {

    private static List<File> fileList = new CopyOnWriteArrayList<>();
    private static Map<Long, String> fileMap = new HashMap<>();

    public static void main(String[] args) throws InterruptedException {
        startAddFile();
        Thread.sleep(3000);
        startUpdateFileMap();
        Thread.sleep(5000);
    }

    // 模拟添加文件
    private static void startAddFile() {
        new Thread(() -> {
            Random random = new Random();
            while (true) {
                IntStream.rangeClosed(1, 5).forEach(i -> {
                    int randomInt = random.nextInt(10);
                    File file = fileList.stream()
                            .filter(item -> item.getId().intValue() == randomInt)
                            .findFirst().orElse(null);
                    if (file == null) {
                        fileList.add(new File()
                                .setId((long) randomInt)
                                .setName("附件" + i)
                                .setCreateTime(System.currentTimeMillis())
                                .setUpdateTime(System.currentTimeMillis()));
                    } else {
                        file.setUpdateTime(System.currentTimeMillis());
                    }
                });
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    System.out.println("线程被中断了");
                }
            }
        }).start();
    }

    // 更新文件信息并计算 MD5
    private static void startUpdateFileMap() {
        new Thread(() -> {
            boolean init = true;
            long lastUpdateTime = System.currentTimeMillis();
            while (true) {
                if (init) {
                    if (!fileList.isEmpty()) {
                        for (File file : fileList) {
                            fileMap.put(file.getId(), MD5.create().digestHex(file.toString(), StandardCharsets.UTF_8));
                        }
                    }
                    init = false;
                } else {
                    long finalLastUpdateTime = lastUpdateTime;
                    List<File> updatedFileList = fileList.stream()
                            .filter(item -> item.getUpdateTime() > finalLastUpdateTime)
                            .collect(Collectors.toList());
                    if (!updatedFileList.isEmpty()) {
                        for (File file : updatedFileList) {
                            fileMap.put(file.getId(), MD5.create().digestHex(file.toString(), StandardCharsets.UTF_8));
                            System.out.println("更新了 fileMap 中 Id 为" + file.getId() + "的数据");
                        }
                    }
                }
                lastUpdateTime = System.currentTimeMillis();
                try {
                    Thread.sleep(3000);
                } catch (InterruptedException e) {
                    System.out.println("线程被中断了");
                }
            }
        }).start();
    }
}
java 复制代码
package com.xsdl.prototype.pojo;

import lombok.Data;
import lombok.experimental.Accessors;

@Data
@Accessors(chain = true)
public class File {

    private Long id;

    private String name;

    private Long createTime;

    private Long updateTime;

}

为了展示样例及避免并发问题,我使用了 CopyOnWriteArrayList 暂存数据库里的真实业务数据,假设没有删除,只有更新和新增。运行实例代码会不断打印出:更新了 fileMap 中 Id 为*的数据。

如果 fileMap 此时还在对外提供访问,希望在某次访问持续期间,元素是不会发生变化的(即不停机更新)该怎么实现呢?目前的逻辑是 fileMap 一直在定时更新,外界每次访问都会是此时的最新的数据,甚至本次访问的开始和结束从 fileMap 中取的值都是不一样的。

我们可以定义一个版本的概念,每次更新 fileMap 的时候创建一个新的版本,在老的 fileMap 被访问结束的时候替换为新版本的 fileMap;如果新版本的 fileMap 每次都查询全量的 fileList 并重新进行计算,实际上是一种资源浪费,我们完全可以复制一份原来的 fileMap,只更新这段时间其中更新的部分。

浅拷贝与深拷贝

具体的解释非常简单,浅拷贝就是拷贝了一份索引表,真实数据并没有真实的创建一份,新旧索引(指针)指向了同一份数据;深拷贝就是索引和数据都重新创建了。

如果你对 redis 比较熟悉,redis 在生成 rdb 时,利用 linux 提供的 cow(写时复制)技术,redis 在 fork 子线程生成 rdb 的时候,重新创建了子线程的页表等,但是依然指向内存中的同一份数据,当这段时间发生了修改或删除,redis 主线程会申请内存真正的再创建一个这样的变量。这样的设计既保证了 RDB 生成的数据为创建完子线程时候的快照备份(子线程创建出来的时候内容已经固定了,无论在生成 RDB 结束前主线程发生了什么操作,数据都与创建出来的时候一致),又兼顾了空间和效率。

我们可以借鉴 redis 的这种做法,在更新 fileMap 时先浅拷贝一份,再把这段时间更新过的内容从新拷贝出的索引中移除,创建一个新的对象存入,这也是最快速达到目的的做法(完全的深拷贝是比浅拷贝要复杂且更加耗时的)。

事实上 Java 里的 CopyOnWriteArrayList 也是借鉴了这种思想,写操作时先复制一份,修改其中要更新的部分。不过它更多是提供了读多写少时的线程安全的 List 的一种实现,保证了每次在迭代过程中不会抛出 ConcurrentModificationException 异常,因为迭代器总是访问一个一致的数据快照。

总结

原型模式提供了一种快速创建复杂对象(创建成本大)的方式,利用对已有对象(原型)进行复制(或者叫拷贝),以达到节省创建时间的目的。

相关推荐
new_daimond2 小时前
设计模式实战-设计模式组合使用
设计模式
Vanranrr2 小时前
Data Wrapper(数据包装器) 设计模式实践
设计模式
爱吃烤鸡翅的酸菜鱼2 小时前
基于多设计模式的状态扭转设计:策略模式与责任链模式的实战应用
java·后端·设计模式·责任链模式·策略模式
charlie1145141912 小时前
精读《C++20设计模式》:重新理解设计模式系列
学习·设计模式·c++20·攻略
Z_z在努力3 小时前
【数据结构前置知识】包装类型
java·数据结构
超级大只老咪3 小时前
数组(Java基础语法)
java·开发语言
new_daimond3 小时前
设计模式-解释器模式详解
java·设计模式·解释器模式
yujkss3 小时前
23种设计模式之【桥接模式】-核心原理与 Java实践
java·设计模式·桥接模式
汤姆yu3 小时前
2025版基于springboot的家政服务预约系统
java·spring boot·后端