带着下面问题学习

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

    为什么要使用单例?

    定义

    单例设计模式:一个类只允许创建一个对象实例,那这个类就是一个单例类,这种设计模式就叫作单例设计模式,简称单例模式。

    如何实现一个单例

    关注点:

  • 构造函数需要是 private 访问权限,这样才能避免外部通过 new 创建实例;

  • 考虑对象创建时的线程安全问题;
  • 考虑是否支持延迟加载;
  • 考虑 getInstance() 性能是否高(是否加锁);

简单介绍几种经典的实现方式:
1、饿汉式
在类加载的时候,instance 静态实例就会创建并初始化好。属于线程安全。但不支持延迟加载。

  1. public class IdGenerator {
  2. private AtomicLong id = new AtomicLong(0);
  3. private static final IdGenerator instance = new IdGenerator();
  4. private IdGenerator() {}
  5. private static IdGenerator getInstance() {
  6. return instance;
  7. }
  8. public long getId() {
  9. return id.incrementAndGet();
  10. }
  11. }

2、懒汉式
懒汉式相对于饿汉式的优势是支持延迟加载。缺点是 getInstance() 加了 synchronized,导致高并发的时候,性能低下。

  1. public class IdGenerator {
  2. private AtomicLong id = new AtomicLong(0);
  3. private static final IdGenerator instance;
  4. private IdGenerator() {}
  5. private static synchronized IdGenerator getInstance() {
  6. if (instance == null) {
  7. instance = new IdGenerator();
  8. }
  9. return instance;
  10. }
  11. public long getId() {
  12. return id.incrementAndGet();
  13. }
  14. }

3、双重检测
解决了饿汉式和懒汉式的问题,既有延迟加载,又支持高并发。

  1. public class IdGenerator {
  2. private AtomicLong id = new AtomicLong(0);
  3. private static final IdGenerator instance;
  4. private IdGenerator() {}
  5. private static IdGenerator getInstance() {
  6. if (instance == null) {
  7. synchronized(IdGenerator.class) { // 类级别锁
  8. if (instance == null) {
  9. instance = new IdGenerator();
  10. }
  11. }
  12. }
  13. return instance;
  14. }
  15. public long getId() {
  16. return id.incrementAndGet();
  17. }
  18. }

4、静态内部类
比双重检测更简单的实现方式,利用了 Java 的静态内部类。在调用 getInstance() 的时候,SingletonHolder 才会被加载。

  1. public class IdGenerator {
  2. private AtomicLong id = new AtomicLong(0);
  3. private static final IdGenerator instance;
  4. private IdGenerator() {}
  5. private static class SingletonHolder {
  6. private static final IdGenerator instance = new IdGenerator();
  7. }
  8. private static IdGenerator getInstance() {
  9. return SingletonHolder.instance;
  10. }
  11. public long getId() {
  12. return id.incrementAndGet();
  13. }
  14. }

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

  1. public enum IdGenerator {
  2. INSTANCE;
  3. private AtomicLong id = new AtomicLong(0);
  4. private long getId() {
  5. return id.incrementAndGet();
  6. }
  7. }

单例存在哪些问题?

  • 单例对OOP的四大特性(封装、抽象、继承、多态)支持不友好

IdGenerator 的使用方式违背了基于接口而非实现的设计原则,也违背了广义上理解的 OOP 的抽象特性。如果后续需要针对不同的业务,采取不同的 ID 生成算法,那么改动就比较大。
除此之外,单例对继承和多态特性的支持也不友好。

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

单例类的创建不需要显式创建、不需要依赖参数传递。这样在代码阅读的时候,需要仔细查看每个函数的代码实现,才能知道这个类到底依赖了哪些单例类。

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

如果我们把单例类,需要在代码中创建两个实例或者多个实例,代码会改动比较大。

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

单例模式的这种硬编码式的使用方式,在写单元测试的时候,无法使用 mock 的方式替换掉单例模式里的外部资源(如 DB)。

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

因为单例模式只会创建一个实例,所以就算有有参数的构造函数,也只会在首次创建实例的时候起作用。

单例与静态类的区别?

静态方法的这种实现思路,并不能解决我们之前提到的问题。实际上,它比单例更加的不灵活,比如,它无法支持延迟加载。

  1. // 静态方法实现方式
  2. public class IdGenerator {
  3. private static AtomicLong id = new AtomicLong(0);
  4. private static long getId() {
  5. return id.incrementAndGet();
  6. }
  7. }
  8. // 使用举例
  9. long id = IdGenerator.getId();

有何替代解决方案

可以通过将单例生成的对象,作为参数传递给函数,可以解决单例隐藏类之间的依赖关系的问题,其他问题还是无法解决。

  1. // 1. 老的使用方式
  2. public demofunction() {
  3. long id = IdGenerator.getInstance().getId();
  4. }
  5. // 2. 新的使用方式:依赖注入
  6. public demofunction(IdGenerator idGenerator) {
  7. long id = idGenerator.getId();
  8. }
  9. // 外部调用 demofunction 的时候,传入 idGenerator
  10. IdGenerator idGenerator = IdGenerator.getInstance();
  11. demofunction(idGenerator);