也来谈谈懒汉和饿汉,详细解析单例模式的六种实现方式!

601 阅读8分钟

这是我参与8月更文挑战的第5天,活动详情查看:8月更文挑战

单例模式

  • 单例模式是一种创建模式,单例类负责自己创建自己的对象并且一个类只有一个实例对象,并且向整个系统提供这个实例.系统可以直接访问这个实例而不需要实例化
  • 单例模式的特点:
    • 单例类只有一个实例
    • 单例类必须自己创建自身的唯一实例
    • 单例类必须给其余系统对象提供创建的唯一实例

单例模式的实现方式

  • 单例模式要保证一个类只有一个实例,并且提供给全局访问,主要用于解决一个全局使用的类频繁创建和销毁的问题,通过判断系统是否存在这个单例来解决这样的问题,如果有这个单例则返回这个单例,否则就创建这个单例,只要保证构造函数是私有的即可
    • 保证一个类只有一个实例: 将该类的构造方法定义为私有方法即可
    • 提供全局一个该实例的访问点: 单例类自己创建实例,提供一个静态方法作为实例的访问点即可
  • 饿汉和懒汉比较:
    • 懒汉: 单例类对象实例懒加载,不会提前创建对象实例,只有在使用对象实例的时候才会创建对象实例
    • 饿汉: 在单例对象实例进行声明引用时就进行实例化创建对象实例
  • 单例模式除去线程不安全的懒汉,通常有五种实现方式:
    • 懒汉
    • 双检锁
    • 饿汉
    • 静态内部类
    • 枚举
  • 一般情况下,直接使用饿汉实现单例模式
  • 如果明确要求懒加载通常使用静态内部类实现单例模式
  • 如果有关于反序列化创建对象会考虑使用枚举实现单例模式
  • 静态类Static :
    • 静态类在第一次运行时直接初始化,也不需要在延迟加载中使用
    • 在不需要维持任何状态,仅仅用于全局访问时,使用静态类的方式更加方便
    • 如果需要被继承或者需要维持一些特定状态下的情况,就适合使用单例模式

线程不安全懒汉

线程安全懒汉

  • 单例模式线程安全懒汉Singleton示例
  • 解决了多线程环境下创建多个实例的问题
  • 存在每次获取实例都需要申请锁的问题,方法效率低下,因为在任何时候只能有一个线程可以调用getInstance() 方法

双检锁

  • 双重检查锁模式: doule checked locking pattern
    • 使用同步块加锁的方法
    • 会有两次检查instance == null
      • 一次在同步块外
      • 一次在同步块内
        • 因为会有多个线程一起进入同步块外的if
        • 如果不在同步块内不进行二次检验就会导致生成多个实例
  • 单例模式双检锁Singleton示例
  • volatile:
    • 对于计算机中的指令而言 ,CPU和编译器为了提升程序的执行效率,通常会按照一定的规则对指令进行优化
    • 如果两条指令互不依赖,那么指令执行的顺序可能不是源码的编写顺序
    • 形如instance = new Instance() 方法创建实例执行分为三步:
      • 分配对象内存空间: 给新创建的Instance对象分配内存
      • 初始化对象: 调用单例类的构造函数来初始化成员变量
      • 设置instance指向新创建的对象分配的内存地址,此时instance != null
        • 因为上面的初始化对象和设置instance指向新创建的对象分配的内存地址不存在数据上的依赖关系,无论哪一步先执行都不会影响最终结果,所以程序在编译时,顺序就会发生改变:
          • 分配对象内存空间
          • 设置instance指向新创建对象分配的内存地址
          • 初始化对象
        • CPU和编译器在指令重排时,不会关心指令重排执行是否影响多线程的执行结果. 如果不加volatile关键字,如果有多个线程访问getInstance() 方法时,如果刚好发生了指令重排,可能会出现以下情况:
          • 当第一个线程获取锁并且进入到第二个if方法后,先分配内存空间,然后instance指向刚刚分配的内存地址,此时instance不等于null. 但是此时instance还没有初始化完成
          • 如果此时有另一个线程调用getInstance() 方法,在第一个if的判断时结果就为false, 就会直接返回没有初始化完成的instance, 这样可能会导致程序NPE异常
    • 使用volatile的原因是禁止指令重新排序:
      • volatile变量进行赋值操作后会有一个内存隔离
      • 读操作不会重排序到内存隔离之中
      • 比如在上面操作中,读操作必须在执行完1,2,3或者1,3,2步骤之后才会执行读取到结果,否则不会读取到相关结果

饿汉

  • 单例模式饿汉Singleton示例
  • 优点:
    • 在单例类中,装载类的时候就创建对象实例.因为单例类的实例声明为staticfinal变量,在第一次加在类到内存中时就会初始化,所以创建实例本身时线程安全的
  • 缺点:
    • 饿汉模式不是一种懒加载模式,即便客户端没有调用getInstance() 方法,单例类也会在类第一次加载时初始化
    • 使用饿汉模式创建单例类实例在某些场景中无法使用:
      • 比如因为饿汉创建的实例声明为final变量
      • 如果单例类Singleton的实例的创建依赖参数或者配置文件
      • 需要在getInstance() 方法之前调用方法为单例类的实例设置参数,此时这种饿汉模式就无法使用

静态内部类

  • 单例模式静态内部类Singleton示例
  • 使用静态内部类模式创建单例类实例是使用JVM机制保证线程安全:
    • 静态单例对象没有作为单例类的成员变量直接实例化,所以当类加载时不会实例化单例类
    • 第一次调用getInstance() 方法时将加载静态内部类Nest. 在静态内部类中定义了一个static类型的变量instance, 这时会首先初始化这个变量
    • 通过JVM来保证线程安全,确保该成员变量只初始化一次
    • 由于getInstance() 方法并没有加线程锁,所以对性能没有什么影响
  • 静态内部类的优点:
    • 静态内部类Nest是私有的,只能通过getInstance() 方法进行访问,所以这是懒加载的
    • 读取实例时不会进行同步锁的获取,性能较好
    • 静态内部类不依赖JDK版本

枚举

  • 单例模式枚举Singleton示例
  • 使用枚举方式实现单例的最大特点是非常简单
  • 可以通过Enum.INSTANCE来访问实例,和getInstance() 方法比较更加简单
  • 枚举的创建默认就是线程安全的方法,而且能防止反射以及反序列化导致重新创建新的对象
    • Enum类内部使用Enum类型判定防止通过反射创建新的对象
    • Enum类通过对象的类型和枚举名称将对象进行序列化,然后通过valueOf() 方法匹配枚举名称找到内存中的唯一对象实例,这样可以防止反序列化时创建新的对象
  • 懒汉式和饿汉式实现的单例模式破坏 : 无论是通过懒汉式还是饿汉式实现的单例模式,都可能通过反射和反序列化破坏掉单例的特性,可以创建多个对象
  • 反射破坏单例模式: 利用反射,可以强制访问单例类的私有构造器,创建新的对象
public static void main(String[] args) {
	// 利用反射获取单例类的构造器
	Constructor<Singleton> constructor = Singleton.class.getDeclaredConstructor();
	// 设置访问私有构造器
 	constructor.setAccessiable(true);
 	// 利用反射创建新的对象
 	Singleton newInstance = constructor.newInstance();
 	// 通过单例模式创建单例对象
 	Singleton singletonInstance = Singleton.getInstance();
 	// 此时这两个对象是两个不同的对象,返回false
 	System.out.println(singletonInstance  == newInstance);
}
  • 反序列化破坏单例模式: 通过readObject() 方法读取对象时会返回一个新的对象实例
public static void main(String[] args) {
	// 创建一个输出流对象
	ObjectOutputStream os = new ObjectOutputStream(new FileOutputStream("Singleton.file"));
	// 将单例类对象写入到文件中
	Singleton singletonInstance = Singleton.getInstance();
	os.writeObject(singleton);
	// 从文件中读取单例对象
	File file = new File("Singleton.file");
	ObjectInputStream is = new ObjectInputStream(new FileInputStream(file));
	Singleton newInstance = (Singleton)is.readObject();
	// 此时这两个对象是两个不同的对象,返回false
	System.out.println(singletonInstance == newInstance);
}