设计模式:关于单例模式你不知道的那些事

226 阅读7分钟

前言

本篇文章主要是讲单例模式,通过这篇文章,可以了解单例模式的如下几个知识点:

  • 什么是单例模式
  • 单例模式的作用
  • 单例模式的实现方式
  • 单例模式实现方式的优缺点
  • 破坏单例的几种方式和解决方法

一. 什么是单例模式

单例模式是一种如何创建对象的设计模式,通过单例模式创建的对象在整个程序中只有一个。

单例模式的核心思想是将对象的构造器私有化,外部不能使用构造器创建对象,内部初始化一个对象,并提供一个外部获取该实例对象的方法。

二. 单例模式的类型

根据对象初始化的时机,可以将单例模式分为两种:

  • 饿汉式:在加载类的时候就将对象初始化
  • 懒汉式:在加载类的时候不初始化对象,在使用的时候才进行初始化

两种方式的区别主要是在线程安全性和资源占用问题上:

  • 线程安全性:
    • 饿汉式在类加载的时候就初始化,由于类加载机制的特性,初始化是线程安全的。
    • 懒汉式在使用的时候才初始化,会存在线程安全问题
  • 资源占用问题:
    • 饿汉式一开始就初始化,如果一直没有使用,就存在资源浪费问题
    • 懒汉式在使用的时候才初始化,不会存在资源浪费问题

三. 饿汉式的实现

饿汉式的实现方式很简单,在类加载的时候直接初始化该类,然后提供一个外部获取实例的方法即可。

public class Singleton{

    //初始化对象
    private static Singleton singleton = new Singleton();

    //提供外部能获取实例的方法
    public  static Singleton getInstance() {

        return singleton;
    }

    //私有化构造器
    private Singleton() {

    }
}

四. 懒汉式的实现

懒汉式是在获取对象的时候再初始化对象,看似简单,但考虑到并发和JVM内部重排序的问题,会存在一些问题。

1. 不考虑线程安全的懒汉式

在不考虑并发和重排序问题的时候,懒汉式实现比较简单。

public class Singleton{

    //只做声明
    private static Singleton singleton;

    //提供外部能获取实例的方法
    public static Singleton getInstance() {
            
          //1.存在线程安全问题
        if (singleton == null) {
            singleton = new Singleton();
        }
        return singleton;
    }

    //私有化构造器
    private Singleton() {

    }
}

上面的单例模式在多线程的时候,在1处会有线程安全问题,可能创建出多个对象。

2. 加锁实现懒汉式

2.1 方法级别同步保证并发

在考虑多线程并发的时候,最简单的方式就是在获取单例的方法上加上synchronized关键字。

public class Singleton{

    //只做声明
    private static Singleton singleton;

    //提供外部能获取实例的方法
    public  static synchronized Singleton getInstance() {

        if (singleton == null) {
           singleton = new Singleton();
        }
        return singleton;
    }

    //私有化构造器
    private Singleton() {

    }
}

这样的方式简单,不会出现问题,但缺点就是整个方法都被锁包围,增加了同步开销,降低了程序的执行效率。

2.2 代码块级别同步保证并发
public class Singleton{

    //只做声明
    private static Singleton singleton;

    //提供外部能获取实例的方法
    public static Singleton getInstance() {

        if (singleton == null) {
            synchronized (Singleton.class) {
                singleton = new Singleton();
            }
        }
        return singleton;
    }

    //私有化构造器
    private Singleton() {

    }
}

上面的代码是能想到的在代码块级别加锁的方式,这种方式在多线程的时候也会存在问题,就是在singleton == null判断的时候,可能多个线程同时执行到这个地方,判断的结果都是true,所以都创建了新的对象。

造成这种问题的原因是代码块加锁的地方不对,如果将singleton == null判断放入同步代码块中就不会出现问题,但是这样又有效率的问题,因为每次获取对象的时候都要获取锁。为了解决这个问题,出现双重检测锁的方案。

2.3 双重检测锁

双重检测锁是为了解决上面出现的问题的方案,既能保证同步代码块范围正确,也保证效率问题。

public class Singleton{

    //只做声明
    private static Singleton singleton;

    //提供外部能获取实例的方法
    public static  Singleton getInstance() {

        if (singleton == null) {
            synchronized (Singleton.class) {
                if(singleton==null){
                    singleton = new Singleton();
                }
            }
        }
        return singleton;
    }

    //私有化构造器
    private Singleton() {

    }
}

通过两次检查,即保证最小粒度的加锁,又能保证线程安全。

但是上面的代码还是存在问题,这个问题是因为JVM指令重排序造成的。

在执行singleton = new Singleton()的时候,最终会被编译为多条汇编指令:

  • 给对象实例分配内存空间
  • 调用对象的构造方法、初始化成员变量
  • 将对象指向分配的内存地址

由于CPU的优化会对执行的指令进行重排序,上面的三个步骤可能最终的执行顺序是1-2-3,也有可能是1-3-2,当执行顺序为1-3-2的时候,当执行第三步时,多个线程进来判断singleton不是null,然后就直接返回,但实际上singleton对象还未初始化完整。

加入volatile解决重排序的问题

要解决上面提到重排序的问题,需要将对象实例定义为线程volatile,这样就可以保证多个线程之间的可见性,防止CPU重排序问题。

public class Singleton{

    //只做声明
    private static volatile Singleton singleton;

    //提供外部能获取实例的方法
    public static Singleton getInstance() {

        if (singleton == null) {
            synchronized (Singleton.class) {
                if (singleton == null) {
                    singleton = new Singleton();
                }
            }
        }
        return singleton;
    }

    //私有化构造器
    private Singleton() {

    }
}

五. 静态内部类实现单例模式

静态内部类实现懒汉式的原理在于静态内部类采用类加载机制保证线程安全的问题,并且静态内部类不会在外部类加载的时候就初始化,而是在外部类调用获取对象实例的时候才会进行类加载,解决资源浪费问题。

public class Singleton{

    static class SingletonInstance{

        public static final Singleton singleton = new Singleton();
    }

    //提供外部能获取实例的方法
    public static Singleton getInstance() {

        return SingletonInstance.singleton;
    }

    //私有化构造器
    private Singleton() {

    }
}

六. 枚举实现单例模式

《Effective Java》中作者强烈推荐使用枚举实现单例模式,因为这种方式是线程安全的,并且只会装载一次,而且序列化、反序列化、克隆、反射都不会创建新的对象,破坏单例。

public class Singleton{

    private enum SingletonEnum{

        INSTANCE;

        private final Singleton instance;

        SingletonEnum() {
            instance = new Singleton();
        }

        private Singleton getInstance() {

            return instance;
        }

    }

    //提供外部能获取实例的方法
    public  static Singleton getInstance() {

        return SingletonEnum.INSTANCE.getInstance();
    }

    //私有化构造器
    private Singleton() {

    }
}

七. 破坏单例的方式

上面的几种实现单例的方法,除了枚举实现单例,其他的方式都可以通过一些手段破坏单例。

1. 反射破坏单例

比如使用双重检测锁的方式实现单例,可以通过反射的方式呢获取单例的构造函数,设置其访问权限,然后通过构造函数创建一个新的对象。


public class SingletonTest{

    public static void main(String[] args) throws Exception {

        Singleton singleton = Singleton.getInstance();

        Class<Singleton> singletonFClass = (Class<Singleton>)                 Class.forName("singleton.Singleton");
        Constructor<Singleton> declaredConstructor = singletonFClass.getDeclaredConstructor(null);
        declaredConstructor.setAccessible(true);
        Singleton singletonReflect = declaredConstructor.newInstance();

        System.out.println(singletonReflect);
        System.out.println(singleton);
        System.out.println(singletonReflect == singleton);
    }
}

输出结果为:

singleton.Singleton@7cef4e59
singleton.Singleton@64b8f8f4
false

可以看到,通过反射,我们可以在程序中创建两个单例对象,这破坏了单例模式原本的目的。

解决方案:在构造函数中增加逻辑校验,如果对象已经存在,则抛出异常,代码如下:

  private Singleton() {

        if (singleton != null) {
            throw new RuntimeException("Singleton constructor is called ");
        }
    }

2. 序列化和反序列化破坏单例

在反序列化的时候,会调用对象的无参构造函数创建一个新的对象,从而破坏单例。

public class SingletonTest1{

    public static void main(String[] args) throws Exception {

        Singleton singleton = Singleton.getInstance();

        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("tempFile"));
        oos.writeObject(singleton);

        File file = new File("tempFile");

        ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file));
        Singleton singletonSerialize = (Singleton) ois.readObject();

        System.out.println(singleton);
        System.out.println(singletonSerialize);
        System.out.println(singleton == singletonSerialize);

    }
}

输出结果:

singleton.Singleton@387c703b
singleton.Singleton@75412c2f
false

解决方案:在单例对象中新增readResolve()方法,并在该方法中调用单例生成的方法即可。在反序列的过程中,会执行ObjectInputStream#readOrdinaryObject()方法,这个方法会判断对象是否包含readResolve()方法,如果包含的话会直接调用该方法获取对象实例。

 private Object readResolve() {
        return getInstance();
}