1、简介

1.1、什么是单例模式?

保证系统中该类仅有一个实例对象,这种设计模式叫做单例模式。

1.2、为什么需要单例模式?

  • 节省公共资源,比如数据库连接池,配置等往往伴随着频繁的创建和销毁,为了节省内存的开销,需要把数据库连接池、配置信息设计成单例模式;
  • 避免对资源的多重占用(不是很理解)。

    2、单例模式的实现

    单例模式有以下5种实现方式,这5种实现方式都遵循以下设计原则:

  • 单例类的构造函数设计成私有的,避免外部调用单例类的构造函数创建对象;

  • 内部有一个private static 类型的实例,当实例在调用getInstance方法时才初始化,称为延迟加载;
  • 向外暴露一个public static Singleton getInstance方法,该方法最终返回单例对象,外部调用该方法获取单例。

    2.1、饿汉模式

    饿汉模式在声明单例时就对其初始化,即非延迟加载,这样带来的问题是如果单例至始至终没有被使用过,则并不需要初始化单例,会造成内存的浪费。

    1. public class SingletonHungry {
    2. private static final SingletonHungry singletonHungry = new SingletonHungry();
    3. private SingletonHungry(){};
    4. public static SingletonHungry getInstance()
    5. {
    6. return singletonHungry;
    7. }
    8. }

    上面的饿汉模式的写法还有线程不安全的问题。

    2.2、懒汉模式

    相比饿汉模式,懒汉模式在单例声明时不初始化,而是在第一次创建单例时才初始化,这就对应了延迟加载。考虑线程安全性,一般对getInstance方法加synchronized关键字同步方法。

    1. public class SingletonLazy {
    2. private static SingletonLazy singletonLazy;
    3. private SingletonLazy(){};
    4. public static synchronized SingletonLazy getInstance()
    5. {
    6. if (singletonLazy == null)
    7. {
    8. singletonLazy = new SingletonLazy();
    9. }
    10. return singletonLazy;
    11. }
    12. }

    2.3、双重检查(DCL,Double Check Lock)

    懒汉模式通过synchronized同步方法保证线程安全性,但synchronized锁的引入会影响性能,可以使用synchronized代码块来更细粒度地加锁,为了避免指令重排序导致生成的单例没有被初始化,使用volatile关键字修饰单例。

    1. public class SingletonDoubleCheck {
    2. private static volatile SingletonDoubleCheck singletonDoubleCheck;
    3. private SingletonDoubleCheck(){};
    4. public static SingletonDoubleCheck getInstance()
    5. {
    6. if (singletonDoubleCheck == null)
    7. {
    8. synchronized (SingletonDoubleCheck.class)
    9. {
    10. if (singletonDoubleCheck == null)
    11. {
    12. singletonDoubleCheck = new SingletonDoubleCheck();
    13. }
    14. }
    15. }
    16. return singletonDoubleCheck;
    17. }
    18. }

    这里解释两个点:
    (1)两次**if (singletonDoubleCheck == null)**分别是为了保证什么?

  • 第一个判空是为了提升性能,如果单例已经被创建了,直接return就可以了,不必再走进synchronized代码块里了,其实只要锁住synchronized代码块里的部分就ok了,这一步只是为了提升性能;

  • 第二个判空是为了确保仅当单例对象第一次调用时被初始化,即保证单例仅初始化一次。具体地,由于getInstance方法本身没有加synchronized,假设线程1先进入第一个if判断,通过后获取同步锁,还没有执行单例的初始化,此时第二个线程调用getInstance方法,线程2也通过了第一个if判断,然后进入阻塞状态,然后线程1执行完单例的初始化释放掉同步锁,线程2获取同步锁进入同步代码块,如果没有第二个判空,此时线程2会执行单例的初始化,这样就违背了单例只能进行一次初始化的定义,因此需要加上第二个if判空,且将单例用volatile关键字修饰,避免线程2又一次进行单例的初始化。

(2)为什么要用**volatile**关键字修饰单例?
根因是singletonDoubleCheck = new SingletonDoubleCheck();这个操作不是原子性的!
Object obj = new Object();实际分为四步(一般把2、3合成一步):

  1. 在栈区创建一个指向Object对象的引用obj
  2. 在堆区开辟一块内存空间给Object对象;
  3. 执行构造函数初始化Object对象;
  4. obj引用指向2中创建的Object对象。

指令重排序会使线程按1、2、3、4执行,也可能按1、4、2、3执行,假设线程1执行了1、2、4,即此时引用obj指向了一块内存空间,并不是null了,这时线程1触发异常退出synchronized代码块并释放锁,线程2进来,由于此时obj不是null了,会直接返回单例,但这时单例并没有走步骤3进行初始化,可能会影响业务逻辑。因此使用volatile关键字,禁止指令重排序,保证返回的单例都是执行了构造函数进行初始化了的。

2.4、静态内部类单例模式

静态内部类单例模式,也是最推荐使用的单例模式,他的工作原理如下:

  • 当任何一个线程第一次调用getInstance()时,都会使LazyHolder静态内部类被加载,此时静态初始化器将执行Singleton的初始化操作,也是延迟加载;
  • 初始化静态数据时,Java提供了的线程安全性保证。(所以不需要任何的同步)

    1. public class SingletonInner {
    2. private SingletonInner(){};
    3. public static SingletonInner getInstance()
    4. {
    5. return LazyHolder.INSTANCE;
    6. }
    7. private static class LazyHolder{
    8. private static SingletonInner INSTANCE = new SingletonInner();
    9. }
    10. }

    2.5、枚举单例模式

    应该是最简洁的单例模式实现方案了,是借助了枚举类已有的实现和机制,优点在于:

  • 写法简单;

  • 防止多次实例化,即使是在面对复杂的序列化或者反射攻击的时候(安全)!(不是很理解)。

枚举方式实现单例模式:

  1. public enum SingletonEnum {
  2. SINGLETON_ENUM;
  3. }

测试Demo:

  1. public class Main {
  2. public static void main(String[] args) {
  3. SingletonEnum singletonEnum1 = SingletonEnum.SINGLETON_ENUM;
  4. SingletonEnum singletonEnum2 = SingletonEnum.SINGLETON_ENUM;
  5. SingletonEnum singletonEnum3 = SingletonEnum.SINGLETON_ENUM;
  6. System.out.println(singletonEnum1.hashCode());
  7. System.out.println(singletonEnum2.hashCode());
  8. System.out.println(singletonEnum3.hashCode());
  9. }
  10. }