读书笔记 | 单例模式总结

一、概述

单例模式(Singleton Pattern)的定义:

保证一个类仅有一个实例,并提供一个访问它的全局访问点。单例模式是一种对象创建型模式。

它的使用场景如下所示:

  • 整个项目需要一个共享访问点或共享数据。
  • 创建一个对象需要消耗的资源过多,比如访问I/O或者数据库等资源。
  • 工具类对象。

在本篇笔记中,将会介绍以下6种单例模式的实现方式:

  • 懒汉模式(线程不安全)
  • 懒汉模式(线程安全)
  • 饿汉模式
  • 双重检查模式(DCL)
  • 静态内部类单例模式
  • 枚举单例

二、实现方式

首先看到最简单,但是也是唯一一个线程不安全的实现——懒汉模式(线程不安全)。

1. 懒汉模式(线程不安全)

这是一种最基本的实现方式,但它是线程不安全的,也就是说只能在单线程的情况下提供正确的单例输出。由于没有加 synchronized 同步锁,严格意义上讲它并不属于单例模式。

之所以叫做懒汉模式(lazying loading)是因为它是在用户第一次调用的时候才会进行初始化,这样子做可以节约部分内存。

它的实现如下:

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

2. 懒汉模式(线程安全)

上面提到的懒汉模式的实现方式是线程不安全的,而要实现线程安全其实非常容易,只需要在 getInstance 方法上加 synchronized 关键字即可,只要锁住了该对象,那么在多线程的情况下它也是安全的,具体实现如下:

public class Singleton {
    private static Singleton instance;

    private Singleton(){
    }

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

虽然这种方式能够在多线程的模式下很好地工作,但是在每次调用 getInstance 方法的时候都需要进行同步,但在大多数情况下我们是用不到同步的,所以这会造成一些不必要的同步开销,一般不推荐采用此方法。

3. 饿汉模式

懒汉模式是在用户第一次调用时才进行实例化,那么饿汉模式就是与之完全相反的——在类加载的时候就已经完成了初始化。它的优点是获取对象的速度快,并且基于类加载机制的模式使其避免了多线程的同步问题,所以它是线程安全的。但它的缺点也显而易见,无论有没有使用该单例实例,在类加载的时候都完成了实例化,如果从始至终都没有使用该实例,那么就会造成内存浪费。

它的实现如下:

public class Singleton {
	// 类加载的时候就完成了初始化
    private static Singleton instance = new Singleton();

    private Singleton(){
    }

    public static Singleton getInstance(){
        return instance;
    }
}

4. 双重检查模式(DCL)

双重检查模式(Double-Check Loading)也是懒汉模式的一种,它会对 Singleton 进行两次判空。需要注意的是使用这种模式实现单例的时候需要在静态成员变量 instance 之前增加修饰符 volatile,以防止在极端情况下单例失效。它的实现如下:

public class Singleton {
    private static volatile Singleton instance;

    private Singleton(){
    }

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

第一次判空是为了避免不必要的同步,因为在单线程的时候我们是不需要进行同步保证的。而第二次判空在同步块内,当 instance 依然为空时,才会去调用构造方法创建对象,这样就能保证线程安全了。

至于为何对 instance 进行 volatile 关键字的修饰,主要原因是防止一种极端情况的发生。我们都知道为了提供程序的性能,JVM 会为我们的代码做一些优化,即指令重排序。我们分析一下第 11 行的代码:instance = new Singleton();,虽然只有一行,但是被编译后它会变成以下 3 条指令:

memory = allocate();       // 1.分配对象的内存空间
ctorInstance(memory);      // 2.初始化对象
instance = memory;          // 3.设置instance指向刚才分配的内存地址

正常情况下,如果这 3 条指令按如上 1、2、3 的顺序进行执行,那么 DCL 就不会出现问题。但 CPU 内部为了提高执行效率,在不影响最终结果的前提下,可能会对上述指令进行重新排序:

memory = allocate();       // 1.分配对象的内存空间
instance = memory;          // 3.设置instance指向刚才分配的内存地址
ctorInstance(memory);     // 2.初始化对象

这里要提一点:

不影响结果的意思是不影响单线程的最终结果,以上述例子为例,在单线程的情况下,2 和 3 之间不存在先行发生关系(即它们之间是互不依赖的)。也就是说在单线程下,先初始化对象再将instance指向分配的内存地址或先将instance指向分配的内存地址在初始化对象最终得到的结果都是一样的。

但是在多线程中,考虑这样的一种情况,在线程 A 调用 getInstance 方法获取实例的时候,假设这时候进行了指令重排序,这是 A 执行了1和3,还未执行2,所以 instance 还未初始化。而此时另一个线程 B 抢占了 CPU 资源,执行到8行,此时判断为 false,那么它就会直接跳到第15行返回 一个还未初始完的 instance 实例,那么此时就会出现问题。

volatile 则可以禁止指令的重排序,从而避免了这种极端情况的发生,虽然损失了一些性能(重排序可以优化性能),但是考虑到程序的正确性这点损失还是值得的。

5. 静态内部类单例模式

我们知道,饿汉模式不能实现延迟加载,无论用不用都会占据内存,而懒汉模式为了线程安全的考虑则添加了同步锁 synchronized,影响了性能。而静态内部类单例模式则是克服了以上两种实现方式的缺点,采用了静态内部类的形式提供单例:

public class Singleton {
    private Singleton(){
    }

    private static Singleton getInstance(){
        return SingletonHolder.sInstance;
    }
    
    private static class SingletonHolder{
        private static final Singleton sInstance = new Singleton();
    }
}

由于 sInstance 不是 Singleton 的成员变量,所以在类加载的时候它并不会被初始化,直到调用 getInstance 方法的时候,虚拟机加载 SingletonHolder 并初始化 sInstance。它的线程安全通过虚拟机的类加载机制进行保证,并且延迟加载的机制也解决了饿汉模式下的问题,所以这是一种比较推荐的单例模式实现方式。

6. 枚举单例

枚举单例的实现如下所示:

public enum  Singleton {
    INSTANCE;
    public void doSomething(){
    }
}

默认枚举实例的创建是线程安全的,并且在任何情况下都是单例,它更加简洁,自动支持序列化机制,防止了反序列化机制,防止多次实例化。它是最佳的单例实现模式,但是在实际中,这种单例的实现方式很少被使用到。

对于单例模式的总结就到此结束了,希望这篇博客能够对您有所帮助~
有问题的话可以在评论区下方给我留言。

参考

本篇博客参考自以下书籍和博客:

《Android 进阶之光》第6章 设计模式

https://www.jianshu.com/p/d628b72e1f2c

猜你喜欢

转载自blog.csdn.net/qq_38182125/article/details/88662197