设计模式开门之单例模式

单例模式归属于设计模式大类(创建型、结构型、行为型)设计模式中的创建型模式。

一个类只允许创建一个对象(实例)那么这个类就是单例类,这个设计模式就是单例设计模式。单例设计模式的这个类提供了一种访问其唯一对象的方式,可以直接访问,不需要实例化该对象,提供了一个全局访问点来访问该实例。

本文从(1)为什么使用单例?(2)如何实现一个单例模式?(3)单例模式可能存在哪些问题?三个方面简单聊聊

为什么使用单例?

处理资源访问冲突

FileWriter本身就是线程安全的,它的内部实现中本身就加了对象级别的锁,因此,在外层调用write()函数的时候,再加对象级别的锁实际上是多此一举。因为不同的Logger对象不共享FileWriter对象,所以,FileWriter对象级别的锁也解决不了数据写入互相覆盖的问题。

那我们该怎么解决这个问题呢?实际上,要想解决这个问题也不难,我们只需要把对象级别的锁,换成类级别的锁就可以了让所有的对象都共享同一把锁。这样就避免了不同对象之间同时调用log()函数,而导致的日志覆盖问题。

public class Logger {
  private FileWriter writer;
  private static final Logger instance = new Logger();  // 只创建一个实例instance

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

// Logger类的应用示例:
public class UserController {
  public void login(String username, String password) {
    // 业务代码
    Logger.getInstance().log(username + "xx");
  }
}

public class OrderController {  
  public void create(OrderVo order) {
    // 业务代码
    Logger.getInstance().log("xx" + order.toString());
  }
}

表示全局唯一类

例如唯一递增号码生成器、调用链路traceId追踪,如果程序中存在两个重复的对象那么就可能造成重复的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();

如何实现一个单例?

饿汉式

在类加载的时候,instance静态实例就已经创建并初始化好了,所以,instance实例的创建过程是线程安全的。不过,这样的实现方式不支持延迟加载(在真正用到IdGenerator的时候,再创建实例)'就是很饥饿,类加载完成的时候 实例也跟着初始化完成了'

public class IdGenerator {
  private AtomicLong id = new AtomicLong(0);
  private static final IdGenerator instance = new IdGenerator(); // 创建单个实例
  private IdGenerator() {}
  public static IdGenerator getInstance() {   //getInstance返回实例
    return instance;
  }
  public long getId() {   // 方法调用
    return id.incrementAndGet();
  }
}

懒汉式

相对于饿汉式来说,懒汉式就是什么时候使用,什么时候再去加载。支持延迟加载

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

以上懒汉式在初始化实例的时候加了一把锁,导致函数的并发度很低,如果是简单调用还可以接受

饿汉式与懒汉式的简单比较:

关于这俩的优缺点,也是仁者见仁智者见智。饿汉式有人认为因为不支持延迟加载,如果实例占用资源多(比如占用内存多)或初始化耗时长(比如需要加载各种配置文件),提前初始化实例是一种浪费资源的行为。最好的方法应该在用到的时候再去初始化。

也有人对饿汉式持支持意见认为有问题就要及早暴露。如果需要很多资源,那么等到需要的时候再去加载,比较耗时的加载过程也可能会影响系统的性能(比如,在响应客户端接口请求的时候,做这个初始化操作,会导致此请求的响应时间变长,甚至超时)以下这句是借鉴江湖道友的:

"如果实例占用资源多,按照fail-fast的设计原则(有问题及早暴露),那我们也希望在程序启动时就将这个实例初始化好。如果资源不够,就会在程序启动的时候触发报错(比如Java中的 PermGen Space OOM),我们可以立即去修复。这样也能避免在程序运行一段时间后,突然因为初始化这个实例占用资源过多,导致系统崩溃,影响系统的可用性。"

双重检测

既支持延迟检测也支持高并发

public class IdGenerator { 
  private AtomicLong id = new AtomicLong(0);
  private static 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();
  }
}

静态内部类

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

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

存在的问题?

对OOP的特性支持不太友好

针对生成唯一的ID自增生成器来说,如果我们想要对不同的业务采用不同的ID生成算法,比如订单类和用户类采用不同的算法,那我们就要修改所有用到生成唯一自增ID的地方,改动较大。可能就需要对不同业务给出同步的ID生成器

会隐藏类之间的依赖关系

由于单例类不需要显式创建,不需要依赖参数传递,在代码中直接调用即可。如果代码比较复杂,调用关系比较隐蔽就比较难以找出这个类依赖了哪些单例类。

......

以上内容有参考成分,欢迎指导学习,不喜勿喷~

posted @ 2024-12-05 14:37  有点儿意思  阅读(49)  评论(0编辑  收藏  举报