2.单例模式,工厂模式,建造者模式,原型模式
创始人
2024-05-20 09:10:17
0

单例模式

单例模式的优点:

  1. 处理资源访问冲突
  2. 表示全局唯一类

实现单例的关键:

  1. 构造函数需要是 private 访问权限的,这样才能避免外部通过 new 创建实例;
  2. 考虑对象创建时的线程安全问题;
  3. 考虑是否支持延迟加载;
  4. 考虑 getInstance() 性能是否高(是否加锁)。

1. 饿汉式

  • 在类加载的时候,instance 静态实例就已经创建并初始化好了,所以,instance 实例的创建过程是线程安全的。
public class IdGenerator {private AtomicLong id = new AtomicLong(0);private static final IdGenerator instance = new IdGenerator();private IdGenerator() {}public static IdGenerator getInstance() {return instance;}public long getId() {return id.incrementAndGet();}
}

提前初始化占用资源,但是让耗时的操作提前到启动的时候完成,解决初始化导致的性能问题

2. 懒汉式

public class IdGenerator {private AtomicLong id = new AtomicLong(0);private static IdGenerator instance;private IdGenerator() {}public static synchronized IdGenerator getInstance() {if (instance == null) {instance = new IdGenerator();}return instance;}public long getId() {return id.incrementAndGet();}
}

如果频繁地用到,那频繁加锁、释放锁及并发度低等问题,会导致性能瓶颈。

3. 双重检测

饿汉式不支持延迟加载,懒汉式有性能问题,不支持高并发。

public class IdGenerator {private AtomicLong id = new AtomicLong(0);private static volatile IdGenerator instance;private IdGenerator() {}public static IdGenerator getInstance() {if (instance == null) {synchronized (IdGenerator.class) { // 此处为类级别的锁if (instance == null) {instance = new IdGenerator();}}}return instance;}public long getId() {return id.incrementAndGet();}
}

CPU 指令重排序可能导致在 IdGenerator 类的对象被关键字 new 创建并赋值给 instance 之后,还没来得及初始化(执行构造函数中的代码逻辑),就被另一个线程使用了(volatile 来禁止指令重排序)。

4. 静态内部类

有点类似饿汉式,但又能做到了延迟加载

public class IdGenerator {private AtomicLong id = new AtomicLong(0);private IdGenerator() {}private static class SingletonHolder {private static final IdGenerator instance = new IdGenerator();}public static IdGenerator getInstance() {return SingletonHolder.instance;}public long getId() {return id.incrementAndGet();}
}

SingletonHolder 是一个静态内部类,当外部类 IdGenerator 被加载的时候,并不会创建
SingletonHolder 实例对象。只有当调用 getInstance() 方法时,SingletonHolder 才会被加载,这个时候才会创建 instance。instance 的唯一性、创建过程的线程安全性,都由 JVM 来保证。所以,这种实现方法既保证了线程安全,又能做到延迟加载。

5. 枚举

通过 Java枚举类型本身的特性,保证了实例创建的线程安全性和实例的唯一性。

public enum IdGenerator {INSTANCE;private AtomicLong id = new AtomicLong(0);public long getId() {return id.incrementAndGet();}
}

单例的缺点

  1. 单例对 OOP 特性的支持不友好(一旦你选择将某个类设计成到单例类,也就意味着放弃了继承和多态这两个强有力的面向对象特性,也就相当于损失了可以应对未来需求变化的扩展性)
  2. 单例会隐藏类之间的依赖关系(单例类不需要显示创建、不需要依赖参数传递,在函数中直接调用就可以了。如果代码比较复杂,这种调用关系就会非常隐蔽。)
  3. 单例对代码的扩展性不友好(单例类只能有一个对象实例,如果需要创建两个实例或
    多个实例,那就要对代码有比较大的改动(比如 想要变成2个线程池,一个是快线程池,一个是慢线程池))
  4. 单例对代码的可测试性不友好
  5. 单例不支持有参数的构造函数

单例的替代方案

  1. 我们还可以用静态方法来实现(比单例更不灵活,不能支持懒加载)
public class IdGenerator {private static AtomicLong id = new AtomicLong(0);public static long getId() {return id.incrementAndGet();}
}// 使用举例long id = IdGenerator.getId();

2.依赖注入(可以解决单例隐藏类之间依赖关系的问题,但是对 OOP 特性、扩展性、可测性不友好等问题,还是无法解决)

public demofunction(IdGenerator idGenerator){long id=idGenerator.getId();}
// 外部调用demofunction()的时候,传入idGeneratorIdGenerator idGenerator=IdGenerator.getInsance();demofunction(idGenerator);}
  1. 我们既可以通过单例模式来强制保证,也可以通过工厂模式、IOC 容器(比如 Spring IOC 容器)来保证,还可以通过程序员自己来保证(自己在编写代码的时候自己保证不要创建两个类对象)

单例类中对象的唯一性的作用范围是进程内的,在进程间是不唯一的。
如果fork一个线程出来,会在新线程也创建一个单例对象

如何实现线程唯一的单例?

  • 通过一个 HashMap 来存储对象,其中 key 是线程 ID,value 是对象。这样我们就可以
    做到,不同的线程对应不同的对象,同一个线程只能对应一个对象。
  • ThreadLocal就是这个的实现

实现集群间单例

  • 把这个单例对象序列化并存储到外部共享存储区(比如文件)。进程在使用这个单例对象的时候,需要先从外部共享存储区中将它读取到内存,并反序列化成对象,然后再使用,使用完成之后还需要再存储回外部共享存储区。
  • 为了保证任何时刻,在进程间都只有一份对象存在,一个进程在获取到对象之后,需要对对象加锁,避免其他进程再将其获取。在进程使用完这个对象之后,还需要显式地将对象从内存中删除,并且释放对对象的加锁。
public class IdGenerator {private AtomicLong id = new AtomicLong(0);private static IdGenerator instance;private static SharedObjectStorage storage = FileSharedObjectStorage(/*入参省略*/);private static DistributedLock lock = new DistributedLock();private IdGenerator() {}public synchronized static IdGenerator getInstance() {if (instance == null) {lock.lock();instance = storage.load(IdGenerator.class);}return instance;}public synchronized void freeInstance() {storage.save(this, IdGeneator.class);instance = null; //释放对象lock.unlock();}public long getId() {return id.incrementAndGet();}
}// IdGenerator使用举例IdGenerator idGeneator = IdGenerator.getInstance();long id = idGenerator.getId();idGenerator.freeInstance();

实现一个多例模式?

public class BackendServer {private long serverNo;private String serverAddress;private static final int SERVER_COUNT = 3;private static final Map serverInstances = new HashMap<>()static {serverInstances.put(1L, new BackendServer(1L, "192.134.22.138:8080"));serverInstances.put(2L, new BackendServer(2L, "192.134.22.139:8080"));serverInstances.put(3L, new BackendServer(3L, "192.134.22.140:8080"));}private BackendServer(long serverNo, String serverAddress) {this.serverNo = serverNo;this.serverAddress = serverAddress;}public BackendServer getInstance(long serverNo) {return serverInstances.get(serverNo);}public BackendServer getRandomInstance() {Random r = new Random();int no = r.nextInt(SERVER_COUNT) + 1;return serverInstances.get(no);}
}

多例模式创建的对象都是同一个类的对象,而工厂模式创建的是不同子类的对象

工厂模式

简单工厂(Simple Factory)

/**
* 根据不同后缀,选择不同的解析器
*/
public class RuleConfigSource {public RuleConfig load(String ruleConfigFilePath) {String ruleConfigFileExtension = getFileExtension(ruleConfigFilePath);IRuleConfigParser parser = RuleConfigParserFactory.createParser(ruleConfigFilif (parser == null) {throw new InvalidRuleConfigException("Rule config file format is not supported: " + ruleConfigFilePath);}String configText = "";
//从ruleConfigFilePath文件中读取配置文本到configText中RuleConfig ruleConfig = parser.parse(configText);return ruleConfig;}private String getFileExtension(String filePath) {
//...解析文件名获取扩展名,比如rule.json,返回jsonreturn "json";}
}public class RuleConfigParserFactory {public static IRuleConfigParser createParser(String configFormat) {IRuleConfigParser parser = null;if ("json".equalsIgnoreCase(configFormat)) {parser = new JsonRuleConfigParser();} else if ("xml".equalsIgnoreCase(configFormat)) {parser = new XmlRuleConfigParser();} else if ("yaml".equalsIgnoreCase(configFormat)) {parser = new YamlRuleConfigParser();} else if ("properties".equalsIgnoreCase(configFormat)) {parser = new PropertiesRuleConfigParser();}return parser;}
}

大部分工厂类都是以“Factory”这个单词结尾的,工厂类中创建对象的方法一般都是 create 开头

我们每次调用 RuleConfigParserFactory 的 createParser() 的时候,都要创建一个新的 parser,所以可以将 parser 事先创建好缓存起来

public class RuleConfigParserFactory {private static final Map cachedParsers = new HashMapcachedParsers.put("json", new JsonRuleConfigParser());cachedParsers.put("xml", new XmlRuleConfigParser());cachedParsers.put("yaml", new YamlRuleConfigParser());cachedParsers.put("properties", new PropertiesRuleConfigParser());}public static IRuleConfigParser createParser(String configFormat) {if (configFormat == null || configFormat.isEmpty()) {return null;//返回null还是IllegalArgumentException全凭你自己说了算}IRuleConfigParser parser = cachedParsers.get(configFormat.toLowerCase());return parser;}
}

尽管简单工厂模式的代码实现中,有多处 if 分支判断逻辑,违背开闭原则,但权
衡扩展性和可读性,这样的代码实现在大多数情况下(比如,不需要频繁地添加 parser,也
没有太多的 parser)是没有问题的。

工厂方法(Factory Method)

可以通过多态避免多的if

public interface IRuleConfigParserFactory {IRuleConfigParser createParser();
}public class JsonRuleConfigParserFactory implements IRuleConfigParserFactory {@Overridepublic IRuleConfigParser createParser() {return new JsonRuleConfigParser();}
}public class XmlRuleConfigParserFactory implements IRuleConfigParserFactory {@Overridepublic IRuleConfigParser createParser() {return new XmlRuleConfigParser();}
}public class YamlRuleConfigParserFactory implements IRuleConfigParserFactory {@Overridepublic IRuleConfigParser createParser() {return new YamlRuleConfigParser();}
}public class PropertiesRuleConfigParserFactory implements IRuleConfigParserFactor {@Overridepublic IRuleConfigParser createParser() {return new PropertiesRuleConfigParser();}
}public class RuleConfigParserFactoryMap { //工厂的工厂private static final Map cachedFactories = nestatic {cachedFactories.put("json", new JsonRuleConfigParserFactory());cachedFactories.put("xml", new XmlRuleConfigParserFactory());cachedFactories.put("yaml", new YamlRuleConfigParserFactory());cachedFactories.put("properties", new PropertiesRuleConfigParserFactory());}public static IRuleConfigParserFactory getParserFactory(String type) {if (type == null || type.isEmpty()) {return null;}IRuleConfigParserFactory parserFactory = cachedFactories.get(type.toLowerCasereturn parserFactory;}
}

工厂方法模式比起简单工厂模式更加符合开闭原则。

那什么时候该用工厂方法模式,而非简单工厂模式呢?

当对象的创建逻辑比较复杂,不只是简单的 new 一下就可以,而是要组合其他类对象,做各种初始化操作的时候,我们推荐使用工厂方法模式,将复杂的创建逻辑拆分到多个工厂类中,让每个工厂类都不至于过于复杂。而使用简单工厂模式,将所有的创建逻辑都放到一个工厂类中,会导致这个工厂类变得很复杂。

抽象工厂(Abstract Factory)

在简单工厂和工厂方法中,类只有一种分类方式。如果还用简单工厂或者工厂方法就会创建过多的工厂类。抽象工厂可以让一个工厂负责创建多个不同类型的对象,有效地减少工厂类的个数。

public interface IConfigParserFactory {IRuleConfigParser createRuleParser();ISystemConfigParser createSystemParser();
//此处可以扩展新的parser类型,比如IBizConfigParser
}
public class JsonConfigParserFactory implements IConfigParserFactory {@Overridepublic IRuleConfigParser createRuleParser() {return new JsonRuleConfigParser();}@Overridepublic ISystemConfigParser createSystemParser() {return new JsonSystemConfigParser();}
}
public class XmlConfigParserFactory implements IConfigParserFactory {@Overridepublic IRuleConfigParser createRuleParser() {return new XmlRuleConfigParser();}@Overridepublic ISystemConfigParser createSystemParser() {return new XmlSystemConfigParser();}
}

工厂模式和 DI 容器有何区别?

一个工厂类只负责某个类对象或者某一组相关类对象(继承自同一抽象类或者接口的子类)的创建,而 DI 容器负责的是整个应用中所有类对象的创建。

DI 容器的核心功能一般有三个:配置解析、对象创建和对象生命周期管理。

  • 将需要由 DI 容器来创建的类对象和创建类对象的必要信息(使用哪个构造函数以及对应的构造函数参数都是什么等等),放到配置文件中。容器读取配置文件,根据配置文件提供的信息来创建对象。
  • 通过“反射”,它能在程序运行的过程中,动态地加载类、创建对象。
  • 多例或者单例对象,是否懒加载,配置创建和销毁时调用的方法。

建造者

当创建对象要传递很多的参数,容易传递错误的参数,并且影响可读性
使用set方法可以解决创建对象参数列表过多的参数,但是不能解决各个参数设置直接的依赖关系,或者约束条件就很难解决(比如maxIdle要大于minIdle)
如果对象在创建好之后,就不能再修改内部的属性值,不能暴露ResourcePoolConfig 的set方法
这个时候就可以使用建造者模式(把校验逻辑放置到 Builder 类中,然后在使用 build() 方法真正创建对象之前,我们把 ResourcePoolConfig 的构造函数改为 private 私有权限,ResourcePoolConfig 没有提供任何 set() 方法,这样我们创建出来的对象就是不可变对象了)

public class ResourcePoolConfig {private String name;private int maxTotal;private int maxIdle;private int minIdle;private ResourcePoolConfig(Builder builder) {this.name = builder.name;this.maxTotal = builder.maxTotal;this.maxIdle = builder.maxIdle;this.minIdle = builder.minIdle;}//...省略getter方法...
//我们将Builder类设计成了ResourcePoolConfig的内部类。
//我们也可以将Builder类设计成独立的非内部类ResourcePoolConfigBuilder。public static class Builder {private static final int DEFAULT_MAX_TOTAL = 8;private static final int DEFAULT_MAX_IDLE = 8;private static final int DEFAULT_MIN_IDLE = 0;private String name;private int maxTotal = DEFAULT_MAX_TOTAL;private int maxIdle = DEFAULT_MAX_IDLE;private int minIdle = DEFAULT_MIN_IDLE;public ResourcePoolConfig build() {// 校验逻辑放到这里来做,包括必填项校验、依赖关系校验、约束条件校验等if (StringUtils.isBlank(name)) {throw new IllegalArgumentException("...");}return new ResourcePoolConfig(this);}public Builder setName(String name) {this.name = name;return this;}public Builder setMaxTotal(int maxTotal) {this.maxTotal = maxTotal;return this;}public Builder setMaxIdle(int maxIdle) {this.maxIdle = maxIdle;return this;}public Builder setMinIdle(int minIdle) {this.minIdle = minIdle;return this;}}
}// 这段代码会抛出IllegalArgumentException,因为minIdle>maxIdleResourcePoolConfig config = new ResourcePoolConfig.Builder().setName("dbconnectionpool").setMaxTotal(16).setMaxIdle(10).setMinIdle(12).build();

工厂模式是用来创建不同但是相关类型的对象(继承同一父类或者接口的一组子类),由给定的参数来决定创建哪种类型的对象。建造者模式是用来创建一种类型的复杂对象,通过设置不同的可选参数,“定制化”地创建不同的对象。

原型模式

如果对象的创建成本比较大,而同一个类的不同对象之间差别不大(大部分字段都相同),在这种情况下,我们可以利用对已有对象(原型)进行复制(或者叫拷贝)的方式来创建新对象,以达到节省创建时间的目的。这种基于原型来创建对象的方式就叫作原型设计模式(Prototype Design Pattern),简称原型模式。
(如果对象中的数据需要经过复杂的计算才能得到(比如排序、计算哈希值),或者需要从 RPC、网络、数据库、文件系统等非常慢速的 IO 中读取)可以使用原型模式。

如果一次有10W条数据需要查询,使用原型模式,不断增加

public class Demo {private HashMap currentKeywords=new HashMap<>();private long lastUpdateTime = -1;public void refresh() {
// 原型模式就这么简单,拷贝已有对象的数据,更新少量差值HashMap newKeywords = (HashMap) currentKeywords.clone();
// 从数据库中取出更新时间>lastUpdateTime的数据,放入到newKeywords中List 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());oldSearchWord.setLastUpdateTime(searchWord.getLastUpdateTime());} else {newKeywords.put(searchWord.getKeyword(), searchWord);}}lastUpdateTime = maxNewUpdatedTime;currentKeywords = newKeywords;}private List getSearchWords(long lastUpdateTime) {
// TODO: 从数据库中取出更新时间>lastUpdateTime的数据return null;}
}

深拷贝和浅拷贝

浅拷贝得到的对象跟原始对象共享数据,而深拷贝得到的是一份完完全全独立的对象

在这里插入图片描述

  • 在 Java 语言中,Object 类的 clone() 方法执行的就是我们刚刚说的浅拷贝。它只会拷贝对象中的基本数据类型的数据(比如,int、long),以及引用对象(SearchWord)的内存地址,不会递归地拷贝引用对象本身。(如果使用浅拷贝可能会存在数据不一致的情况)

深拷贝的两种方法

  • 递归拷贝对象、对象的引用对象以及引用对象的引用对象……直到要拷贝的对象只包含基本数据类型数据,没有引用对象为止。
public class Demo {private HashMap currentKeywords = new HashMap<>();private long lastUpdateTime = -1;public void refresh() {
// Deep copyHashMap newKeywords = new HashMap<>();for (HashMap.Entry e : currentKeywords.entrySet()) {SearchWord searchWord = e.getValue();SearchWord newSearchWord = new SearchWord(searchWord.getKeyword(), searchWord.getCount(), searchWord.getLastUnewKeywords.put(e.getKey(), newSearchWord);}
// 从数据库中取出更新时间>lastUpdateTime的数据,放入到newKeywords中List 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());oldSearchWord.setLastUpdateTime(searchWord.getLastUpdateTime());} else {newKeywords.put(searchWord.getKeyword(), searchWord);}}lastUpdateTime = maxNewUpdatedTime;currentKeywords = newKeywords;}private List getSearchWords(long lastUpdateTime) {
// TODO: 从数据库中取出更新时间>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 中的老对象。

    public void refresh() {// Shallow copyHashMap newKeywords = (HashMap) currentKeywords.clone();// 从数据库中取出更新时间>lastUpdateTime的数据,放入到newKeywords中List 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;}

如果要拷贝的对象是不可变对象,浅拷贝共享不可变对象是没问题的,但对于可变对象来说,浅拷贝得到的对象和原始对象会共享部分数据,就有可能出现数据被修改的风险,

相关内容

热门资讯

监控摄像头接入GB28181平... 流程简介将监控摄像头的视频在网站和APP中直播,要解决的几个问题是:1&...
Windows10添加群晖磁盘... 在使用群晖NAS时,我们需要通过本地映射的方式把NAS映射成本地的一块磁盘使用。 通过...
protocol buffer... 目录 目录 什么是protocol buffer 1.protobuf 1.1安装  1.2使用...
在Word、WPS中插入AxM... 引言 我最近需要写一些文章,在排版时发现AxMath插入的公式竟然会导致行间距异常&#...
【PdgCntEditor】解... 一、问题背景 大部分的图书对应的PDF,目录中的页码并非PDF中直接索引的页码...
Fluent中创建监测点 1 概述某些仿真问题,需要创建监测点,用于获取空间定点的数据࿰...
educoder数据结构与算法...                                                   ...
MySQL下载和安装(Wind... 前言:刚换了一台电脑,里面所有东西都需要重新配置,习惯了所...
修复 爱普生 EPSON L4... L4151 L4153 L4156 L4158 L4163 L4165 L4166 L4168 L4...
MFC文件操作  MFC提供了一个文件操作的基类CFile,这个类提供了一个没有缓存的二进制格式的磁盘...