拿下单例模式

设计模式,漫漫长路

Posted by Timer on October 3, 2021

单例模式解决了什么问题

一个全局使用的变量反复创建和销毁

单例模式的使用场景

  • Web的配置文件

  • 数据库连接池

  • 多线程的线程池

  • Spring的Bean

  • Web的日志

实现方式 1.0:懒汉

class Singleton {
    private static Singleton instance;

    private Singleton() {
    }

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

懒加载。线程不安全。单线程场景性能好。

实现方式 1.1:懒汉加锁

class Singleton {
    private static Singleton instance;

    private Singleton() {
    }

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

懒加载。线程安全。性能差,因为每一次获得对象都要加锁。

实现方式 2.0:饿汉

class Singleton {
    private static Singleton instance = new Singletion();

    private Singleton() {}

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

非懒加载。线程安全。同上,性能差。

实现方式 3.0:DCL(Double-Check Locking)

public final class Singleton implements Serializable{
    private volatile static Singleton singleton;

    private Singleton() {
    }

    public static Singleton getSingleton() {
        if (singleton == null) {
            synchronized (Singleton.class) {
                if (singleton == null) {
                    singleton = new Singleton();
                }
            }
        }
        return singleton;
    }
    
    public Object readResolve(){
        return singleton;
    }
}

懒加载。线程安全。高性能。

  • 高效的原因在于只对第一次创建对象用了锁,后续获得对象无锁。

  • 使用synchronized是因为多线程情况下,可能多个线程判断对象为null,所以同步创建对象的代码,一次保证只有一个线程进。

  • 第二次判定对象是否为空是因为,当第一个进入的线程创建好对象之后,后续线程不需要重复创建。

  • synchronized已经可以保证原子性和可见性,使用votatile是为了对付指令重排序。

    创建对象要三步:

    1. 分配空间
    2. 初始化对象
    3. 把空间地址给引用

    如果线程A在创建对象的时候,编译器为了优化进行了指令重排序,将创建对象的步骤变为:

    1. 分配空间
    2. 把空间地址给引用
    3. 初始化对象

    那么可能出现,线程A在第二步结束的时候,还未初始化对象,线程BCD已经在请求获得对象了,这个时候引用已经被赋值,所以会直接返回instance,但是此时对象依然为空。

DCL相关问题

  1. 为什么类要加final?

    防止子类重写get方法

  2. 如果实现了序列化接口,要怎么防止反序列化破坏单例?

    加入readResovle方法

  3. 为什么构造方法要私有?这样能阻止反射创建新对象吗?

    防止外界创建单例模式类的对象,只能类中调用get方法才能创建实例。

    不能阻止,反射本质是获得Class对象,也就是C++里的instanceKlass,相当于直接获得这个类的类对象,直接修改访问修饰符就能把private变成public,根本不是一个维度的对手。

  4. 下列代码能保证线程安全吗?

    private volatile static Singleton singleton = new Singleton();

    当然能,静态对象初始化是在类加载时候的,和线程安全能发生的时间完全不重合。

实现方式 4.0:枚举单例模式

Effective Java中说的最好的单例模式实现方式。

public enum  EnumSingleton {
    INSTANCE;
    public EnumSingleton getInstance(){
        return INSTANCE;
    }
}

枚举单例模式相关问题

  1. 枚举单例是怎么限制实例个数的?

    从反编译中可以看出来,枚举类中的成员变量都是static,所以实例唯一。

  2. 枚举单例创建的时候是否有并发问题?

    因为是static实例,所以创建的时候是类加载阶段,避免指令重排序等问题!无并发问题。

  3. 枚举单例能否用反射破坏?

    不可以。反射在用newInstance创建对象的时候,会检查是否是ENUM修饰的,如果是就抛出异常。

  4. 枚举单例能不能用反序列化破坏单例?

    不可以,枚举类Enum实现了readObject方法,一旦调用反序列化,会抛出以下异常。

     throw new InvalidObjectException("can't deserialize enum");
    
  5. 枚举单例是懒汉式还是饿汉式?

    饿汉,底层是静态变量,所以不是懒加载。 «««< HEAD =======

5923cbf3cea8405b5e4eeb936edd4afdcea7f87c