设计模式(一) 单例设计模式

单例设计模式是什么?

单例模式,是一种常用的软件设计模式。在它的核心结构中只包含一个被称为单例的特殊类。通过单例模式可以保证系统中,应用该模式的类一个类只有一个实例。即一个类只有一个对象实例。

在常用的设计模式中,Singleton 是唯一一个能够用短短几十行代码完整实现的模式

单例设计模式类结构图

img

单例设计模式实现

实现关键

  • 将构造方法私有化,使其不能在类的外部通过new关键字实例化该类对象。

  • 在该类内部产生一个唯一的实例化对象,并且将其封装为private static类型。

  • 定义一个静态方法返回这个唯一对象。

单例模式有很多实现方式,下面将会带大家一一实现,并体会这些方式的优劣点

不好的解法一:只适用于单线程环境

/**
 * @Version: V1.0.0
 * @Description: 懒汉式加载 只适合单线程环境
 * Singleton的静态属性instance中,只有instance为null的时候才创建一个实例,构造函数私有,确保每次都只创建一个,避免重复创建。
 */
public class Singleton1 {

    private static Singleton1 instance = null;

    private Singleton1() {
    }

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

缺点:只在单线程的情况下正常运行,在多线程的情况下,就会出问题。例如:当两个线程同时运行到判断instance是否为空的if语句, 并且instance确实没有创建好时,那么两个线程都会创建一个实例

不好的解法二:虽然可以在多线程环境中工作,但效率不高

/**
 * @Version: V1.0.0
 * @Description: 懒汉式加载 可以在多线程环境中工作,但效率不高
 * 为了保证在多线程环境下我们还是只能得到类型的一个实例,需要加上一个同步锁
 * 在解法一的基础上加上了同步锁,使得在多线程的情况下可以用。
 * 例如:当两个线程同时想创建实例,由于在一个时刻只有一个线程能得到同步锁,当第一个线程加上锁以后,第二个线程只能等待。
 * 第一个线程发现实例没有创建,创建之。第一个线程释放同步锁,第二个线程才可以加上同步锁,
 * 执行下面的代码。
 * 由于第一个线程已经创建了实例,所以第二个线程不需要创建实例。保证在多线程的环境下也只有一个实例
 */
public class Singleton2 {

    private static Singleton2 instance = null;

    private Singleton2() {
    }

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

缺点:每次通过getInstance方法得到singleton实例的时候都有一个试图去获取同步锁的过程。而众所周知,加锁是很耗时的。能避免则避免。

可行的解法:加锁前后两次判断实例是否已经存在

/**
 * @Version: V1.0.0
 * @Description: 懒汉式加载 加锁前后两次判断实例是否已经存在
 * 只有当instance为null即没有创建时,需要加锁操作,创建一次实例。当实例被创建,则无需试图加锁。
 */
public class Singleton3 {

    private static Singleton3 instance = null;

    private Singleton3() {
    }

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

        }
        return instance;
    }
}

缺点:这样实现的代码比较复杂,容易出错,我们还有更优秀的解法

推荐的解法一:利用静态构造函数

/**
 * @Version: V1.0.0
 * @Description: 饿汉式加载
 * 在初始化静态变量instance时候创建一个实例
 */
public class Singleton4 {

    private static Singleton4 instance = new Singleton4();

    private Singleton4() {
    }

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

缺点:静态实例instance并不是在第一次调用属性 Singleton4.getInstance()方法的时候被创建,而是在第一次用到 Singleton4 的时候就会被创建。如果我们在Singleton4 中里面写一个静态的方法不需要创建实例,它仍然会早早的创建一次实例。而降低内存的使用率。

推荐的解法二:利用静态内部类实现按需创建实例

/**
 * @Version: V1.0.0
 * @Description: 饿汉式加载
 * 定义一个私有的内部类,在第一次用这个嵌套类时,会创建一个实例。
 * 而类型为SingletonHolder的类,只有在Singleton.getInstance()中调用,
 * 由于私有的属性,他人无法使用SingleHolder,不调用Singleton.getInstance()就不会创建实例。
 */
public class Singleton5 {

    private static Singleton5 instance = new Singleton5();

    private Singleton5() {
    }

    private static class SingletonHolder {
        private final static Singleton5 instance = new Singleton5();
    }

    public static Singleton5 getInstance() {
        return SingletonHolder.instance;
    }
}

推荐的解法三:利用枚举实现单例

/**
 * @Version: V1.0.0
 * @Description: 利用枚举创建单例
 * (1)自由序列化。
 * (2)保证只有一个实例。
 * (3)线程安全。
 */
public enum Singleton6 {
    INSTANCE;

    private Object data;

    public Object getData() {
        return data;
    }

    public void setData(Object data) {
        this.data = data;
    }

    public Singleton6 getInstance() {
        return INSTANCE;
    }
}

单例模式问题及解决

我们知道单例模式的目的就是在一个系统中只会存在一个实例。无论懒汉式创建单例和饿汉式创建单例,在面对反射攻击和序列化攻击时都会破坏单例,下面是使用饿汉式创建的单例在面对反射攻击和序列化攻击时单例被破坏的示例:

import java.io.*;
import java.lang.reflect.Constructor;

/**
 * @Version: V1.0.0
 * @Description: 模拟反射攻击和序列化攻击破坏单例场景
 */
public class SingletonDestoryTest implements Serializable {

    public static void main(String[] args) throws Exception {
        // 通过饿汉式获取单例对象
        SingletonDestoryTest instance = SingletonDestoryTest.getInstance();

        // 通过反射获取单例对象
        Class instanceClass = SingletonDestoryTest.class;
        Constructor constructor = instanceClass.getDeclaredConstructor();
        constructor.setAccessible(true);
        SingletonDestoryTest newInstance1 = (SingletonDestoryTest) constructor.newInstance();

        // 序列化到文件
        ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("F:\file"));
        out.writeObject(instance);

        // 反序列化读取对象
        ObjectInputStream in = new ObjectInputStream(new FileInputStream("F:\file"));
        SingletonDestoryTest newInstance2 = (SingletonDestoryTest) in.readObject();

        System.out.println("单例模式创建的对象:" + instance);
        System.out.println("反射创建的对象:" + newInstance1);
        System.out.println("反序列化创建的对象:" + newInstance2);
        System.out.println("原对象与反射创建对象是否相等:" + (instance == newInstance1));
        System.out.println("原对象与反序列化创建对象是否相等:" + (instance == newInstance2));
    }

    private static SingletonDestoryTest instance = new SingletonDestoryTest();

    private SingletonDestoryTest() {
    }

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

执行结果:

image-20200801222327909

我们看到在不加任何其他操作的情况下,普通单例模式创建的对象无法防止反射攻击和序列化破坏

单例模式的最佳实践

使用枚举类实现单例模式,借助枚举类天然的在IO类与反射类方面的特殊处理,可以天然的防反射攻击,防序列化与反序列化破坏。这样实现的单例模式既简单又安全。

枚举类防反射攻击

import java.io.*;
import java.lang.reflect.Constructor;

/**
 * @Version: V1.0.0
 * @Description: 模拟反射攻击和序列化攻击破坏单例场景
 */
public class SingletonEnumTest {

    public static void main(String[] args) throws Exception {
        // 通过饿汉式获取单例对象
        Singleton6 instance = Singleton6.getInstance();
        instance.setData(new Object());

        // 通过反射获取单例对象
        Class instanceClass = Singleton6.class;
        Constructor constructor = instanceClass.getDeclaredConstructor();
        constructor.setAccessible(true);
        Singleton6 newInstance1 = (Singleton6) constructor.newInstance();

        System.out.println("单例模式创建的对象:" + instance);
        System.out.println("反射创建的对象:" + newInstance1);
        System.out.println("原对象与反射创建对象是否相等:" + (instance == newInstance1));
    }

}

执行结果:

image-20200801223956112

枚举类不允许通过反射方式构造实例,会抛出 NoSuchMethodException 异常

枚举类防序列化与反序列化破坏

import java.io.*;
import java.lang.reflect.Constructor;

/**
 * @Version: V1.0.0
 * @Description: 模拟反射攻击和序列化攻击破坏单例场景
 */
public class SingletonEnumTest {

    public static void main(String[] args) throws Exception {
        // 通过饿汉式获取单例对象
        Singleton6 instance = Singleton6.getInstance();
        instance.setData(new Object());

        // 序列化到文件
        ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("F:\file"));
        out.writeObject(instance);

        // 反序列化读取对象
        ObjectInputStream in = new ObjectInputStream(new FileInputStream("F:\file"));
        Singleton6 newInstance2 = (Singleton6) in.readObject();

        System.out.println("单例模式创建的对象:" + instance);
        System.out.println("反序列化创建的对象:" + newInstance2);
        System.out.println("原对象与反序列化创建对象是否相等:" + (instance == newInstance2));
    }

}

执行结果:

image-20200801224246248

总结

单例模式的创建有三种方式:懒汉式、饿汉式、枚举类

懒汉式:在用到的时候才会创建对象实例,需要加锁保证多线程并发安全问题,同时需要双重校验,将锁粒度控制到最小,保证程序执行效率。

饿汉式:饿汉式是在类加载的时候就是创建该对象的实例,不管该对象有没有被用到,饿汉式的升级版本是使用静态内部类,将创建对象的实例延迟到第一次使用该对象,避免了饿汉式浪费空间的问题。

枚举类:枚举类是 effect java 中推荐的方法,防反射攻击,防序列化与反序列化破坏,使用枚举类创建单例对象即简单又安全。

参考链接

《剑指offer》

设计模式之单例模式七(使用枚举类的最佳实践)

原文地址:https://www.cnblogs.com/dtdx/p/13424289.html