单例模式的四种实现的简单分析

691 阅读4分钟

单例模式是 Java 中最简单的设计模式之一。在单例模式下,只允许这个类具有一个实例。一般由该类负责创建自己的对象,同时确保只有单个对象被创建。

重点:

  • 只允许有一个实例。既然只允许有一个实例,那构造方法肯定是不能让外人随随便便就能调用的。所以构造函数一定是private修饰的。
  • 提供别的类获取实例的方法。一般都用getInstance,方法名就叫获取实例,简单粗暴。

下面简单介绍一下我所知道的四种实现单例模式的方法:

一、实现方式介绍

1. 饿汉式

饿汉式是指在初始化这个类的时候就创建单例对象,为啥叫饿汉式呢,大概是因为比较着急吧,急着去创建对象,就像看到食物的我一样。。。 在初始化的时候就直接new instance,不存在创建时候的线程安全问题,私有构造函数

private final static HungerSingleton1 instance = new HungerSingleton1();

2. 懒汉式

懒汉式,式如其名,不到用的时候不创建,就像懒汉一样,作业不到要交不做,项目不到快交付就划水。。。关键是在getInstance的时候创建对象。
①第一种,最简单的实现

private static LazySingleton1 instance;

public static LazySingleton1 getInstance(){
    if (instance == null){
i        nstance = new LazySingleton1();
    }
        return instance;
}

问题:存在线程安全问题,多线程同时调用getInstance()会有多个实例产生。
②第二种 直接synchronized修饰getInstance方法,给方法加锁。不过会带来性能问题,所有getInstance的时候都会加锁。
③第三种 双重检验(DoubleCheck )

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

在instance为空的时候初始化,先加锁,然后再次判断instance是否为空,为空才进行初始化。
第二次判断instance是否为空:一开始多线程调用getInstance 此时尚未初始化,但大家都开始排队拿锁,如果不加第二次判断,后面拿到锁的线程会重复创建对象,和第一种并没有什么差别。

3.静态内部类

利用静态内部类进行懒加载(个人认为可以算是懒汉式的第四种实现)

private static class StaticClassSingletonInstance{
    private final static StaticClassSingleton instance = new StaticClassSingleton();
}

public static StaticClassSingleton getInstance(){
    return StaticClassSingletonInstance.instance;
}

利用JVM的机制实现线程安全。虽然对静态内部类来说,似乎有点像饿汉式,但是在调用getInstance之前,并没有加载静态内部类。而在调用getInstance的时候JVM去初始化静态内部类且JVM会保证初始化的线程安全

4.枚举

public enum EnumSingleton {
    INSTANCE;
}

使用的时候直接EnumSingleton.INSTANCE就完事了,也是利用java的机制实现的。按照我的理解,枚举类并不是在调用的时候才创建的实例,所以枚举实现我觉得算是饿汉式的补充。因为在调用枚举类的static方法的时候,依然会创建实例。

二、四种实现方式分析

上述四种实现方式中,1、2、3依然存在两个问题:

  • 反序列化的时候,可能会出现两个实例。
  • 通过反射可以创建出两个实例。

1.序列化的问题

代码:

LazySingleton2 singleton1 = LazySingleton2.getInstance();

ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("singletonFile"));
outputStream.writeObject(singleton1);

ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream("singletonFile"));

LazySingleton2 newSingleton = (LazySingleton2) inputStream.readObject();

System.out.println(singleton1);
System.out.println(newSingleton);

输出结果: 解决方案:

public Object readResolve(){
    return instance;
}

原因:在序列化的时候,如果发现该类有readResolve就不会执行构造方法了,会直接执行这个方法返回实例。

2.反射的问题

代码:

Class clazz = HungerSingleton1.class;
Constructor constructor = clazz.getDeclaredConstructor();

constructor.setAccessible(true);
Object newInstance = constructor.newInstance();
System.out.println(newInstance);
System.out.println(HungerSingleton1.getInstance());

输出结果: 解决方案: 在1,3两种实现中,可以通过在构造方法中添加以下方法的方式实现防止反射生成新实例:

if (instance != null){
    throw new RuntimeException("禁止反射调用构造方法");
}

而懒汉式通过该方案有可能会有问题,下面解释一下这个方案:
第1种实现:
由于在初始化加载的时候就已经创建了实例,所以在反射调用构造方法的时候一定会抛出这个RuntimeException。
第2种实现:
由于是懒加载,所以如果在调用之前进行反射,是可以创建出另一个实例的
第3种实现:
同样是在使用的时候才进行初始化,但使用这种方法能成功的原因是由于他是静态内部类的熟悉,在判断的时候是 StaticClassSingletonInstance.instance != null 在StaticClassSingletonInstance.instance的时候,会初始化静态内部类。由于对内部类来说是饿汉式实现,所以在判断的时候就会先一步去创建一份实例,所以一样会抛出RuntimeException。

枚举的方式不会出现序列化和反射的问题

个人结论

推荐使用枚举,简单粗暴。如果存在只使用静态方法,暂时用不上实例的情况,或者需要懒加载,我觉得静态内部类的方式比较好。

最后,欢迎大佬指正的补充呀~