设计模式(一) 单例模式

129 阅读5分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第1天,点击查看活动详情

1 不要去 开课吧 问原因就 自己百度去

2 上节回顾/本节重点/上节思考题

2.1 本节讲解

1. 实现一个线程安全的单例
2 为什么要使用单例?
3 单例存在哪些问题?
4 单例与静态类的区别?
5 有何替代的解决方案?
6 单例这种设计模式存在哪些问题?

2.3 上节 思考题解答

4 如何实现一个单例?

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

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

4.3 双重检测

虽然都是类锁,但懒汉式每次方法调用都会尝试获取锁,双重检测则不会。


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

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

4.5 枚举

最简单的实现方式,基于枚举类型的单例实现。这种实现方式通过 Java 枚举类型本身的特性,保证了实例创建的线程安全性和实例的唯一性。

Java规范字规定,每个枚举类型及其定义的枚举变量在JVM中都是唯一的,因此在枚举类型的序列化和反序列化上,Java做了特殊的规定。在序列化的时候Java仅仅是将枚举对象的name属性输到结果中,反序列化的时候则是通过java.lang.Enum的valueOf()方法来根据名字查找枚举对象。也就是说,序列化的时候只将DATASOURCE这个名称输出,反序列化的时候再通过这个名称,查找对应的枚举类型,因此反序列化后的实例也会和之前被序列化的对象实例相同。


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

5 单例模式的缺点

5.1 单例对 OOP 特性的支持不友好

单例的使用是直接获取了实际对象,故违背了面向接口编程的设计原则,无法保证开放-闭合原则。

5.2 单例会隐藏类之间的依赖关系

要表达的是某个类要是依赖了单例,不仔细阅读代码是不容易发现的,降低了代码的可读性,不能一眼就看出这个类对其他类的依赖关系。

5.3 单例对代码的扩展性不友好

5.4 单例对代码的可测试性不友好

5.5 单例不支持有参数的构造函数

配置文件与单例对象有良好的适应性,不再需要传入可变参数来来创建单例对象,而是采用静只加载一次加的配置文件决定,对使用者可以隐藏参数的传递,若要修改参数,就修改配置文件后重启


public class Config {
  public static final int PARAM_A = 123;
  public static final int PARAM_B = 245;
}

public class Singleton {
  private static Singleton instance = null;
  private final int paramA;
  private final int paramB;

  private Singleton() {
    this.paramA = Config.PARAM_A;
    this.paramB = Config.PARAM_B;
  }

  public synchronized static Singleton getInstance() {
    if (instance == null) {
      instance = new Singleton();
    }
    return instance;
  }
}

6 有上面这么多问题 那怎么办呢?

6.1 有何替代解决方案?

6.1.1 静态方法实现方式


// 静态方法实现方式
public class IdGenerator {
  private static AtomicLong id = new AtomicLong(0);
  
  public static long getId() { 
    return id.incrementAndGet();
  }
}
// 使用举例
long id = IdGenerator.getId();

6.1.2 依赖注入

基于新的使用方式,我们将单例生成的对象,作为参数传递给函数(也可以通过构造函数传递给类的成员变量),可以解决单例隐藏类之间依赖关系的问题。不过,对于单例存在的其他问题,比如对 OOP 特性、扩展性、可测性不友好等问题,还是无法解决。


// 1. 老的使用方式
public demofunction() {
  //...
  long id = IdGenerator.getInstance().getId();
  //...
}

// 2. 新的使用方式:依赖注入
public demofunction(IdGenerator idGenerator) {
  long id = idGenerator.getId();
}
// 外部调用demofunction()的时候,传入idGenerator
IdGenerator idGenerator = IdGenerator.getInsance();
demofunction(idGenerator);

7 前面思考和应用场景解答 (在此之前 请再次思考前面的问题 )

7.1 实现一个线程安全的单例

一般使用 饿汉式 / 静态内部类

7.2 为什么要使用单例?

防止资源重复加载 造成浪费

7.3 单例存在哪些问题?

扩展性/多态/继承/抽象 等方面都收到影响,要仔细根据业务思考

7.4 单例与静态类的区别?

扩展性/多态/继承/抽象 等方面都收到影响,要仔细根据业务思考    

7.5 有何替代的解决方案?

static / 依赖注入 / 工厂类等方式   

7.6 springboot 开发的使用 单例模式能用在哪里

8 总结:

9 下节预知

  1. 工厂模式

11 思考题

单例的在实际场景中 怎么使用的,可以评论打出