一、创建型模式

单例模式

  • 为什么要使用单例?
    1、处理资源访问冲突

     public class Logger {
            
            
        private FileWriter writer;
        public Logger() {
            
            
            File file = new File("/Users/wangzheng/log.txt");
            writer = new FileWriter(file, true); //true表示追加写入
        }
         public void log(String message) {
            
            
              writer.write(mesasge);
         }
     }
    

    所有的日志都写入到同一个文件 /Users/wangzheng/log.txt 中。


    在 Web 容器的 Servlet 多线程环境下,如果两个 Servlet 线程同时分别执行 login() 和 create() 两个函数,并且同时写日志到 log.txt 文件中,那就有可能存在日志信息互相覆盖的情况。


    对象级别的锁,一个对象在不同的线程下同时调用 log() 函数,会被强制要求顺序执行。但是,不同的对象之间并不共享同一把锁。


    类级别的锁,让所有的对象都共享同一把锁。这样就避免了不同对象之间同时调用 log() 函数,而导致的日志覆盖问题。


    分布式锁、并发队列(BlockingQueue)多个线程同时往并发队列里写日志,一个单独的线程负责将并发队列中的数据,写入到日志文件实现起来也稍微有点复杂。


    相对于这两种解决方案,单例模式的解决思路就简单一些了。单例模式相对于之前类级别锁的好处是,不用创建那么多 Logger 对象,一方面节省内存空间,另一方面节省系统文件句柄


    将 Logger 设计成一个单例类,程序中只允许创建一个 Logger 对象,所有的线程共享使用的这一个 Logger 对象,共享一个 FileWriter 对象,而 FileWriter 本身是对象级别线程安全的,也就避免了多线程情况下写日志会互相覆盖的问题。

    public class Logger {
            
            
    private FileWriter writer;
    private static final Logger instance = new Logger();
    
        private Logger() {
            
            
            File file = new File("/Users/wangzheng/log.txt");
            writer = new FileWriter(file, true); //true表示追加写入
        }
    
        public static Logger getInstance() {
            
            
            return instance;
        }
    
        public void log(String message) {
            
            
            writer.write(mesasge);
        }
    }
    

    2、表示全局唯一类

    数据在系统中只应保存一份,那就比较适合设计为单例类; 配置信息类

  • 如何实现一个单例?

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

    1、饿汉式

    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();
      }
    }
    

    在类加载的时候,instance 静态实例就已经创建并初始化好了,所以,instance 实例的创建过程是线程安全的。不过,这样的实现方式不支持延迟加载
    将耗时的初始化操作,提前到程序启动的时候完成,这样就能避免在程序运行的时候,再去初始化导致的性能问题
    在程 序启动时就将这个实例初始化好。如果资源不够,就会在程序启动的时候触发报错,可以立即去修复。这样也能避免在程序运行一段时间后,突然因为初始化这个实例占用资源过多,导致系统崩溃,影响系统的可用性。

    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();
      }
    }
    

    给 getInstance() 这个方法加了一把大锁(synchronzed),导致这个函数的并发度很低,如果频繁地用到,那频繁加锁、释放锁及并发度低等问题,会导致性能瓶颈,这种实现方式就不可取了。


    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();
      }
    }
    

    既支持延迟加载、又支持高并发的单例实现方式
    只要 instance 被创建之后,即便再调用 getInstance() 函数也不会再进入到加锁逻辑中了。所以,这种实现方式解决了懒汉式并发度低的问题


    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。insance 的唯一性、创建过程的线程安全性,都由 JVM 来保证。所以,这种实现方法既保证了线程安全,又能做到延迟加载。


    5、枚举

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

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

  • 单例模式中的唯一性:进程内唯一,进程间不唯一

  • 实现线程唯一的单例: 线程内唯一,线程间不唯一

/**
 * 通过一个 HashMap 来存储对象,其中 key 是线程 ID,value 是对象
 *  ThreadLocal 工具类,可以更加轻松地实现线程唯一单例。
 *  不过,ThreadLocal 底层实现原理也是基于下面代码中所示的 HashMap。
 **/
public class IdGenerator {
     
     
  private AtomicLong id = new AtomicLong(0);
  private static final ConcurrentHashMap<Long, IdGenerator> instances = new ConcurrentHashMap<>();
  private IdGenerator() {
     
     }
  public static IdGenerator getInstance() {
     
     
      Long currentThreadId = Thread.currentThread().getId();
      instances.putIfAbsent(currentThreadId, new IdGenerator());
      return instances.get(currentThreadId);
   }
  public long getId() {
     
     
      return id.incrementAndGet();
  }
} 
  • 多例模式

“多例”指的就是一个类可以创建多个对象,但是个数是有限制的.
实现:通过一个 Map 来存储对象类型和对象之间的对应关系,来控制对象的个数。枚举类型也相当于多例模式.

工厂模式

工厂模式是用来创建不同但是相关类型的对象(继承同一父类或者接口的一组子类),由给定的参数来决定创建哪种类型的对象。


应用场景:
当创建逻辑比较复杂,是一个“大工程”的时候,考虑使用工厂模式,封装对象的创建过程,将对象的创建和使用相分离。如果创建对象的逻辑并不复杂,那我们就直接通过 new 来创建对象就可以了,不需要使用工厂模式。

第一种情况:类似规则配置解析的例子,代码中存在 if-else 分支判断,动态地根据不同的类型创建不同的对象。针对这种情况,考虑使用工厂模式,将这一大坨 if-else 创建对象的代码抽离出来,放到工厂类中。

当每个对象的创建逻辑都比较简单的时候,使用简单工厂模式,将多个对象的创建逻辑放到一个工厂类中。


当每个对象的创建逻辑都比较复杂的时候,为了避免设计一个过于庞大的简单工厂类,使用工厂方法模式,将创建逻辑拆分得更细,每个对象的创建逻辑独立到各自的工厂类中


第二种情况:不需要根据不同的类型创建不同的对象,但是,单个对象本身的创建过程比较复杂,比如要组合其他类对象,做各种初始化操作。在这种情况下,也可以考虑使用工厂模式,将对象的创建过程封装到工厂类中。
  • 判断要不要使用工厂模式的标准

封装变化:创建逻辑有可能变化,封装成工厂类之后,创建逻辑的变更对调用者透明。
代码复用:创建代码抽离到独立的工厂类之后可以复用。
隔离复杂性:封装复杂的创建逻辑,调用者无需了解如何创建对象。
控制复杂度:将创建代码抽离出来,让原本的函数或类职责更单一,代码更简洁。

  • 工厂模式和 DI 容器有何区别 DI 容器底层最基本的设计思路就是基于工厂模式的。DI
    容器相当于一个大的工厂类,负责在程序启动的时候,根据配置(要创建哪些类对象,每个类对象的创建需要依赖哪些其他类对象)事先创建好对象。当应用程序需要使用某个类对象的时候,直接从容器中获取即可。正是因为它持有一堆对象,所以这个框架才被称为“容器”。

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

      配置解析:用哪个构造函数以及对应的构造函数参数都是什么等等),放到配置文件中。容器读取配置文件,根据配置文件提供的信息来创建对象。
      对象创建:将所有类对象的创建都放到一个工厂类(反射)中完成就可以了,比如 BeansFactory
      对象的生命周期管理:

      简单工厂模式有两种实现方式,一种是每次都返回新创建的对象,另一种是每次都返回同一个事先创建好的对象,也就是所谓的单例对象。在 Spring 框架中,可以通过配置 scope 属性,来区分这两种不同类型的对象。scope=prototype 表示返回新创建的对象,scope=singleton 表示返回单例对象。
      配置对象是否支持懒加载。如果 lazy-init=true,对象在真正被使用到的时候(比如:BeansFactory.getBean(“userService”))才被被创建;如果 lazy-init=false,对象在应用启动的时候就事先创建好。
      可以配置对象的 init-method 和 destroy-method 方法,比如 init-method=loadProperties(),destroy-method=updateConfigFile()。DI 容器在创建好对象之后,会主动调用 init-method 属性指定的方法来初始化对象。在对象被最终销毁之前,DI 容器会主动调用 destroy-method 属性指定的方法来做一些清理工作,比如释放数据库连接池、关闭文件。

建造者模式

建造者模式是用来创建一种类型的复杂对象,可以通过设置不同的可选参数,“定制化”地创建不同的对象。


把校验逻辑放置到 Builder 类中,先创建建造者,并且通过 set() 方法设置建造者的变量值,然后在使用 build() 方法真正创建对象之前,做集中的校验,校验通过之后才会创建对象。除此之外,我们把
ResourcePoolConfig 的构造函数改为 private 私有权限。这样我们就只能通过建造者来创建 ResourcePoolConfig 类对象。并且,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("...");
            }
            if (maxIdle > maxTotal) {
    
    
                throw new IllegalArgumentException("...");
            }
            if (minIdle > maxTotal || minIdle > maxIdle) {
    
    
                throw new IllegalArgumentException("...");
            }
            return new ResourcePoolConfig(this);
        }

        public Builder setName(String name) {
    
    
            if (StringUtils.isBlank(name)) {
    
    
                throw new IllegalArgumentException("...");
            }
            this.name = name;
            return this;
        }

        public Builder setMaxTotal(int maxTotal) {
    
    
            if (maxTotal <= 0) {
    
    
                throw new IllegalArgumentException("...");
            }
            this.maxTotal = maxTotal;
            return this;
        }

        public Builder setMaxIdle(int maxIdle) {
    
    
            if (maxIdle < 0) {
    
    
                throw new IllegalArgumentException("...");
            }
            this.maxIdle = maxIdle;
            return this;
        }

        public Builder setMinIdle(int minIdle) {
    
    
            if (minIdle < 0) {
    
    
                throw new IllegalArgumentException("...");
            }
            this.minIdle = minIdle;
            return this;
        }
    }
}

// 这段代码会抛出IllegalArgumentException,因为minIdle>maxIdle
//  ResourcePoolConfig config = new ResourcePoolConfig.Builder()
//          .setName("dbconnectionpool")
//          .setMaxTotal(16)
//          .setMaxIdle(10)
//          .setMinIdle(12)
//          .build();
  • 应用场景:
    如果一个类中有很多属性,为了避免构造函数的参数列表过长,影响代码的可读性和易用性,我们可以通过构造函数配合 set() 方法来解决。但是,如果存在下面情况中的任意一种,我们就要考虑使用建造者模式了。

1、把类的必填属性放到构造函数中,强制创建对象的时候就设置。如果必填的属性有很多,把这些必填属性都放到构造函数中设置,那构造函数就又会出现参数列表很长的问题。如果我们把必填属性通过 set() 方法设置,那校验这些必填属性是否已经填写的逻辑就无处安放了。


2、如果类的属性之间有一定的依赖关系或者约束条件,我们继续使用构造函数配合 set() 方法的设计思路,那这些依赖关系或约束条件的校验逻辑就无处安放了。


3、如果希望创建不可变对象,对象在创建好之后,就不能再修改内部的属性值,要实现这个功能,我们就不能在类中暴露 set() 方法。构造函数配合 set() 方法来设置属性值的方式就不适用了。

原型模式

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

  • 应用场景
    实际上,创建对象包含的申请内存、给成员变量赋值这一过程,本身并不会花费太多时间,或者说对于大部分业务系统来说,这点时间完全是可以忽略的。但是,如果对象中的数据需要经过复杂的计算才能得到(比如排序、计算哈希值),或者需 要从 RPC、网络、数据库、文件系统等非常慢速的 IO 中读取,这种情况下,我们就可以利用原型模式,从其他已有对象中直接拷贝得到,而不用每次在创建新对象的时候,都重复执行这些耗时的操作。

  • 实现方式:深拷贝和浅拷贝
    1、浅拷贝

    浅拷贝只会复制对象中基本数据类型数据和引用对象的内存地址,不会递归地复制引用对象,以及引用对象的引用对象。如果要拷贝的对象是不可变对象,浅拷贝共享不可变对象是没问题的,但对于可变对象来说,浅拷贝得到的对象和原始对象会共享部分数据,就有可能出现数据被修改的风险,也就变得复杂多了。

    2、深拷贝

    第一种方法:递归拷贝对象、对象的引用对象以及引用对象的引用对象……直到要拷贝的对象只包含基本数据类型数据,没有引用对象为止。
    第二种方法:先将对象序列化,然后再反序列化成新的对象。

次在创建新对象的时候,都重复执行这些耗时的操作。

  • 实现方式:深拷贝和浅拷贝
    1、浅拷贝

    浅拷贝只会复制对象中基本数据类型数据和引用对象的内存地址,不会递归地复制引用对象,以及引用对象的引用对象。如果要拷贝的对象是不可变对象,浅拷贝共享不可变对象是没问题的,但对于可变对象来说,浅拷贝得到的对象和原始对象会共享部分数据,就有可能出现数据被修改的风险,也就变得复杂多了。

    2、深拷贝

    第一种方法:递归拷贝对象、对象的引用对象以及引用对象的引用对象……直到要拷贝的对象只包含基本数据类型数据,没有引用对象为止。
    第二种方法:先将对象序列化,然后再反序列化成新的对象。

    深拷贝比起浅拷贝来说,更加耗时,更加耗内存空间,但是没有充分的理由,不要为了一点点的性能提升而使用浅拷贝。

猜你喜欢

转载自blog.csdn.net/weixin_46488959/article/details/126919072