创建型:单例设计模式1

目录介绍
  • 01.单例模式介绍
  • 02.单例模式定义
  • 03.单例使用场景
  • 04.思考几个问题
  • 05.为什么要使用单例
  • 06.处理资源访问冲突
  • 07.表示全局唯一类

01.单例模式介绍

  • 单例模式是应用最广的模式

    • 也是最先知道的一种设计模式,在深入了解单例模式之前,每当遇到如:getInstance()这样的创建实例的代码时,我都会把它当做一种单例模式的实现。
  • 单例模式特点

    • 构造函数不对外开放,一般为private
    • 通过一个静态方法或者枚举返回单例类对象
    • 确保单例类的对象有且只有一个,尤其是在多线程的环境下
    • 确保单例类对象在反序列化时不会重新构造对象

02.单例模式定义

  • 保证一个类仅有一个实例,并提供一个访问它的全局访问点

03.单例使用场景

  • 应用中某个实例对象需要频繁的被访问。
  • 应用中每次启动只会存在一个实例。如账号系统,数据库系统。

04.思考几个问题

  • 网上有很多讲解单例模式的文章,但大部分都侧重讲解,如何来实现一个线程安全的单例。重点还是希望搞清楚下面这样几个问题。

    • 为什么要使用单例?
    • 单例存在哪些问题?
    • 单例与静态类的区别?
    • 有何替代的解决方案?

05.为什么要使用单例

  • 单例设计模式(Singleton Design Pattern)理解起来非常简单。

    • 一个类只允许创建一个对象(或者实例),那这个类就是一个单例类,这种设计模式就叫作单例设计模式,简称单例模式。
  • 重点看一下,为什么我们需要单例这种设计模式?它能解决哪些问题?接下来我通过两个实战案例来讲解。

    • 第一个是处理资源访问冲突;
    • 第二个是表示全局唯一类;

06.处理资源访问冲突

  • 实战案例一:处理资源访问冲突

    • 先来看第一个例子。在这个例子中,我们自定义实现了一个往文件中打印日志的 Logger 类。具体的代码实现如下所示:

      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);
      }
      }
      
      // Logger类的应用示例:
      public class UserController {
      private Logger logger = new Logger();
      
      public void login(String username, String password) {
      // ...省略业务逻辑代码...
      logger.log(username + " logined!");
      }
      }
      
      public class OrderController {
      private Logger logger = new Logger();
      
      public void create(OrderVo order) {
      // ...省略业务逻辑代码...
      logger.log("Created an order: " + order.toString());
      }
      }
  • 看完代码之后,先别着急看我下面的讲解,你可以先思考一下,这段代码存在什么问题。
  • 在上面的代码中,我们注意到,所有的日志都写入到同一个文件 /Users/wangzheng/log.txt 中。在 UserController 和 OrderController 中,我们分别创建两个 Logger 对象。在 Web 容器的 Servlet 多线程环境下,如果两个 Servlet 线程同时分别执行 login() 和 create() 两个函数,并且同时写日志到 log.txt 文件中,那就有可能存在日志信息互相覆盖的情况。
  • 为什么会出现互相覆盖呢?我们可以这么类比着理解。在多线程环境下,如果两个线程同时给同一个共享变量加 1,因为共享变量是竞争资源,所以,共享变量最后的结果有可能并不是加了 2,而是只加了 1。同理,这里的 log.txt 文件也是竞争资源,两个线程同时往里面写数据,就有可能存在互相覆盖的情况。
  • 那如何来解决这个问题呢?我们最先想到的就是通过加锁的方式:给 log() 函数加互斥锁(Java 中可以通过 synchronized 的关键字),同一时刻只允许一个线程调用执行 log() 函数。具体的代码实现如下所示:

    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) {
        synchronized(this) {
          writer.write(mesasge);
        }
      }
    }
  • 不过,你仔细想想,这真的能解决多线程写入日志时互相覆盖的问题吗?

    • 答案是否定的。这是因为,这种锁是一个对象级别的锁,一个对象在不同的线程下同时调用 log() 函数,会被强制要求顺序执行。但是,不同的对象之间并不共享同一把锁。在不同的线程下,通过不同的对象调用执行 log() 函数,锁并不会起作用,仍然有可能存在写入日志互相覆盖的问题。

07.表示全局唯一类

  • 从业务概念上,如果有些数据在系统中只应保存一份,那就比较适合设计为单例类。
  • 比如,配置信息类。在系统中,我们只有一个配置文件,当配置文件被加载到内存之后,以对象的形式存在,也理所应当只有一份。
  • 再比如,唯一递增 ID 号码生成器,如果程序中有两个对象,那就会存在生成重复 ID 的情况,所以,我们应该将 ID 生成器类设计为单例。

    import java.util.concurrent.atomic.AtomicLong;
    public class IdGenerator {
      // AtomicLong是一个Java并发库中提供的一个原子变量类型,
      // 它将一些线程不安全需要加锁的复合操作封装为了线程安全的原子操作,
      // 比如下面会用到的incrementAndGet().
      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();
      }
    }
    
    // IdGenerator使用举例
    long id = IdGenerator.getInstance().getId();
  • 实际上,今天讲到的两个代码实例(Logger、IdGenerator),设计的都并不优雅,还存在一些问题。

更多内容


杨充
221 声望42 粉丝