设计模式:单例模式


设计模式:单例模式

一、定义

单例模式:一个类只允许创建一个对象(或者叫实例),那么这个类就是一个单例类,这种设计模式就叫作单例设计模式

二、实现(例子:ID生成器)

饿汉式

  • 定义:通过静态变量在类加载的时候就创建并初始化好instance
  • 优点:线程安全(类加载初始化)
  • 缺点:不支持延迟加载(需要使用时才初始化)
  • 总结:如果初始化耗时长,推荐在程序启动的时候会就初始化好(而不是在使用时才初始化,会影响性能),同时程序启动的时候还能暴露出资源可能不够的错误(而不是在使用时才报错,会影响系统可用性)
  • 代码示例
    public class IdGenerator {
        private AtomicLong id = new AtomicLong(0);
        //静态实例创建并初始化好
        private static final IdGenerator instance = new IdGenerator();
        //private的构造方法
        private IdGenerator(){}
        //public的get方法
        public static IdGenerator getInstance() {
            return instance;
        }
    
        public long getId() {
            return id.incrementAndGet();
        }
    }

懒汉式

  • 定义:在需要使用的时候才初始化
  • 优点:支持延迟加载(需要使用时才初始化)
  • 缺点getInstance()方法加了synchronized关键字,导致这个函数的病发度很低(不适合频繁调用的场景)
  • 总结:可以延迟加载,但如果频繁调用,会导致性能瓶颈
  • 代码示例
    public class IdGenerator {
        private AtomicLong id = new AtomicLong(0);
        //静态实例只声明不初始化
        private static IdGenerator instance;
        //private的构造方法
        private IdGenerator() {}
        //public的get方法
        public static synchronized IdGenerator getInstance() {
            if(instance == null) {
                instance = new IdGenerator();
            }
            return instance;
        }
        
        public long getId() {
            return id.incrementAndGet();
        }
    }
    

双重检测

  • 定义:相比懒汉式instance不为空的时候将不再会加锁(而是初始化的时候才会加锁)
  • 优点instance为null的时候才会加锁(初始化)
  • 缺点:可能由于指令重排序导致new完IdGenerator之后,赋值给instance之后还没来得及初始化(new了但还没执行构造函数代码,这时候其他线程拿到的instance就不是null了)
  • 总结:判断instance为空才加锁,同时还可以给instancevolatile关键字(防止指令重排序)
  • 代码示例
    public class IdGenerator {
        private AtomicLong id = new AtomicLong(0);
        //静态实例只声明不初始化
        private static IdGenerator instance;
        //private的构造方法
        private IdGenerator() {}
        public static IdGenerator getInstance() {
            if (instance == null) {
                //instance不为空才加锁
                synchronized (IdGenerator.class) {
                    if (instance == null) {
                        instance = new IdGenerator();
                    }
                }
            }
            return instance;
        }
    
        public long getId() {
            return id.incrementAndGet();
        }
    }

静态内部类

  • 定义:利用Java的静态内部类,类似饿汉式的同时,又能做到延迟加载
  • 原理:初始化IdGenerator的时候并不会创建SingletonHolder(只会等调用getInstance()方法才会加载SingletonHolder并创建instance)。保证instance的唯一性、保证创建过程的线程安全性(JVM实现)
  • 优点:既保证了线程安全,又能做到延迟加载
public class IdGenerator {
    private AtomicLong id = new AtomicLong(0);
    //private的构造方法
    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();
    }
}

枚举

  • 定义:基于枚举类型的单例实现(通过Java枚举类型本身的特性)
  • 优点:简单,保证了实例创建的线程安全性和实例的唯一性
public enum IdGenertor {
    INSTANCE;
    private AtomicLong id = new AtomicLong(0);
    
    public long getId(){
        
    }
}

三、单例存在哪些问题?

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

    • 违背抽象特性(因为直接获取了单例对象,而不是面向接口编程)
    • 继承多态特性的支持不友好
  • 单例会隐藏类之间的依赖关系

    • 单例类:无需显示创建、无需依赖参数传递
    • 非单例类:可通过构造函数、参数传递等方式看出类之间的依赖
  • 单例对代码的扩展性不友好

    • 背景:单例类只能有一个对象实例
    • 问题:如果有场景需要创建多个实例,那么对代码会有较大改动
    • 场景:数据库连接池、线程池等资源池(可能要分开处理)
    • 总结:单例类在某些场景下会影响代码的扩展性、灵活性
  • 单例对代码的可测试性不友好

    • 可能依赖比较重的外部资源(如DB),导致写单测的时候无法通过mock方式替换掉
    • 单例类的成员变量(如IdGenerator的id变量)相当于全局变量,测试时可能会收到其他地方的修改
  • 单例不支持有参数的构造函数

    • 例子:比如创建一个连接池的单例对象,就无法通过参数来指定连接池的大小

    • 解决方案

      • 1.通过init函数去初始化单例对象

        public class Singleton {
          private static Singleton instance = null;
          //自定义参数A
          private final int paramA;
          //自定义参数B
          private final int paramB;
        
          //带参构造函数
          private Singleton(int paramA, int paramB) {
            this.paramA = paramA;
            this.paramB = paramB;
          }
        
          //实例外部get方法
          public static Singleton getInstance() {
            if(instance == null) {
              throw new RuntimeException("run init() first");
            }
        
            return instance;
          }
        
          //初始化函数(带参数)
          public synchronized static Singleton init (int paramA, int paramB) {
            if(instance != null) {
              throw new RuntimeException("duplicate init");
            }
        
            instance = new Singleton(paramA, paramB);
            return instance;
          }
        }
        
        //调用例子
        public class Demo {
          public static void main(String[] args) {
            //先init
            Singleton.init(10, 20);
            //再使用
            Singleton singleton = Singleton.getInstance();
          }
        }
      • 2.参数放到getInstance()方法(问题:多次调用后续传的参数无效,只取初始化的参数使用)

        public class Singleton {
            private static Singleton instance = null;
          //自定义参数A
            private final int paramA;
          //自定义参数B
          private final int paramB;
          
          //有参构造函数
          private Singleton(int paramA, int paramB) {
              this.paramA = paramA;
              this.paramB = paramB;
          }
          
          //带参数的外部get方法
          public synchronized static Singleton getInstance(int paramA, int paramB) {
              if(instance == null) {
                  instance = new Sinleton(paramA, paramB);
              }
              return instance;
          }
        }
        
        public class Demo {
          public static void main(String[] args) {
            //初始化
            Singleton singleton = Singleton.getInstance(10, 20);
            
            //无效,Singleton只初始化一次(10,20),所以singleton2取出还是(10,20)
            Singleton singleton2 = Singleton.getInstance(30, 40);
          }
        }
      • 3.(推荐使用)将参数放在配置文件(全局变量)中,在类实例化时加载

        public class Config {
          public static final int PARAM_A = 10;
          public static final int PARAM_B = 20;
        
        }
        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;
          }
        }

四、单例有什么替代解决方案?

  • 使用工厂模式
  • 使用IOC容器

文章作者: GaryLee
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 GaryLee !
  目录