面试中单例模式引发的思考

0、前言

考察:并发?类加载?序列化?

什么是单例模式:保证一个类只有一个实例,并且提供一个全局可以访问的入口。

为什么要用单例模式:

  • 节省内存,节省计算。
  • 保证结果的正确

1、单例模式的应用场景

  • 无状态的工具类:日志工具,字符创工具
  • 全局信息类:全局计数,环境变量

2、单例模式的5种写法

【饿汉式】

public class Singleton01 {

    private static Singleton01 singleton = new Singleton01();
    private Singleton01() {
    }
    public static Singleton01 getInstance() {
        return singleton;
    }
}

添加了静态代码块——把类加载这部分逻辑放到静态中

public class Singleton02 {

    private static Singleton02 singleton02;
    static {
        singleton02 = new Singleton02();
    }
    private Singleton02(){}
    public Singleton02 getInstance(){
        return singleton02;
    }
}

以上两种都是类初始化的时候就直接加载了。但是如果这个类从始至终都没有使用过的话就会造成内存浪费。

【懒汉模式】 :判断类不为空的话就初始化一个对象

public class Singleton03 {

    private static Singleton03 singleton03;

    private Singleton03(){}

    public static Singleton03 getInstance(){
        if (singleton03 == null){
            singleton03 = new Singleton03();
        }
        return singleton03;
    }
}

上面那个线程不安全:多线程情况下回创建多个实例

public class Singleton03 {

    private static Singleton03 singleton03;

    private Singleton03(){}

    public static synchronized Singleton03 getInstance(){
        if (singleton03 == null){
            singleton03 = new Singleton03();
        }
        return singleton03;
    }
}

以上代码 多线程情况下效率不高

public class Singleton04 {
    private static Singleton04 singleton04;
    private Singleton04(){}
    public static Singleton04 getInstance(){
        if (singleton04 == null){
            synchronized (Singleton04.class) {
                singleton04 = new Singleton04();
            }
        }
        return singleton04;
    }

}

提高了效率 但是有可能会多实例

public class Singleton05 {
    
    //线程安全
    private static volatile Singleton05 singleton05;
    private Singleton05(){}
    public static Singleton05 getInstance(){
        if (singleton05 == null){  //去掉后导致串行
            synchronized (Singleton05.class) {
                if (singleton05 == null) { //去掉后
                    singleton05 = new Singleton05();
                }
            }
        }
        return singleton05;
    }

}

双重检查 能够解决上面破坏实例化的问题。

JVM 在这个方法中做了几件事情:顺序可能是1-2-3   或者  1-3-2

1、给singleton分配内存空间

2、调用singleton的构造函数等来初始化singleton

3、将singleton对象指向分配的内存空间(执行完这一步的话singleton就不是null了)

volatile关键字要注意下哦!能够防止上面说的重排序而导致的报错

public class Singleton06 {
    
    //静态内部类的写法
    private Singleton06(){}

    private static class SingletonInstance{
        private static final Singleton06 singleton06 = new Singleton06();
    }

    public static Singleton06 getInstance(){
        return SingletonInstance.singleton06;
    }
}
public enum  Singleton07 {
    INSTANCE;
    public void whateverMethod(){}
}

枚举主要是为了防止反序列化 和 反射来导致多个实例被构造出来。

参考https://www.cnblogs.com/cjn123/p/12159536.html

3、枚举类写法的优点

《Effective Java》 用私有构造器或者枚举类型强化SingleTon(单例)属性

单例(singleton)就是一个只实例化一次的类。使类成为单例可能会使它的测试变得困难,因为除非它实现了作为其类型的接口,否则不可能用模拟实现来代替这个单例。下面是几种实现单例的方法:

1、共有静态成员是final类型

public class SingletonEffective01 {
    public static final SingletonEffective01 INSTANCE = new SingletonEffective01();
    private SingletonEffective01(){}
    public void lTB(){}
}

私有构造器之后执行一次,实例化Elvis.INSTANCE属性,由于缺少公有(public)的或者受保护(protected)的构造器,所以Elvis一旦被实例化,就只会存在一个Elvis的实例(注:反射是可以实现多次调用私有的构造器,若需要抵御这种攻击,则可以修改私有构造器,让它在被创建第二个实例时,抛出异常,或直接返回第一个实例对象)。

2、公有的成员是一个静态方法。

public class SingletonEffective02 {

    public static final SingletonEffective02 INSTANCE = new SingletonEffective02();
    private SingletonEffective02(){}
    public static SingletonEffective02 getInstance(){return INSTANCE;} //新增了这个方法
    public void lTB(){}

    //为防止反序列化时又创建了一个新的实例,需要在反序列化是直接返回新的数据

    //1、防止反序列化获取多个对象的漏洞。
    //2、无论是实现Serializable接口,或是Externalizable接口,当从I/O流中读取对象时,readResolve()方法都会被调用到。
    //3、实际上就是用readResolve()中返回的对象直接替换在反序列化过程中创建的对象
    private Object readResolve(){
        return INSTANCE;
    }
}

这个方式可以通过公有静态方法将类改为非单例,但用户代码不需要改变。

为了使利用这其中一种方法实现的SingleTon类变成是可序列化的(Serializable),仅仅在申明上加上“implements Serializable”是不够的。为了维护并保证SingleTon,必须申明所有实例都是瞬时的(transient),并提供一个readResolve方法,否者,每次反序列化一个序列化的实例时,都会创建一个新的实例,比如说,在我们的实例中,会导致“假冒的Elvis”。为了防止这种情况,要在Elvis类中加入readResolve方法

3、使用枚举实现单例

public class SingletonEffective03{
    private SingletonEffective03(){}
    public static SingletonEffective03 getInstance(){
        return Singleton.INSTANCE.getInstance();
    }

    private enum Singleton{
        INSTANCE;

        private SingletonEffective03 singleton;
        //JVM会保证此方法绝对只调用一次
        Singleton(){
            singleton = new SingletonEffective03();
        }
        public SingletonEffective03 getInstance(){
            return singleton;
        }
    }
}

4、引发的一些小知识点

final关键字:

volatile关键字:

原文地址:https://www.cnblogs.com/cjn123/p/12148351.html