重走JAVA之路(二):面试中的单例模式(从入门到放弃)

1,518 阅读5分钟

前言

说到单例设计模式,大家应该都比较熟悉,也能说个一二三,单例单例,无非就是 保证一个类只有一个对象实例嘛,一般就是私有化构造函数,然后再暴露一个方法提供一个实例,确实没错,但是怎么样保证一个单例的安全性呢,私有构造函数,那如果反射强势调用呢?再比如序列化一个对象后,反序列化呢?生成的对象是否还是一样的?

1.常见的单例模式

单例模式现在的写法确实也是有蛮多种,总结一下,大概有如下几种:

  • 懒汉式写法
  • 饿汉式写法
  • DCL写法(双重判断)
  • 静态内部类写法
  • 枚举类写法

那么每种写法到底有什么区别呢?哪种才是最适合的,话不多说,直接撸代码~

1.1 懒汉式写法

/**
 * @FileName: com.example.hik.lib
 * @Desription: 描述功能
 * @Anthor: taolin
 * @Version V2.0 <描述当前版本功能>
 */
public class SingleInstanceDemo {
    private static SingleInstanceDemo sSingleInstanceDemo;
    private SingleInstanceDemo(){

    }
    public synchronized static SingleInstanceDemo getInstance(){
        if (sSingleInstanceDemo==null)
            sSingleInstanceDemo = new SingleInstanceDemo();
        return sSingleInstanceDemo;
    }
}

代码很简单,这种方式是线程安全的,但是很明显,每次调用方法,都需要先获得同步锁,性能比较低,不建议这么写

1.2 饿汉式写法

/**
 * @FileName: com.example.hik.lib
 * @Desription: 描述功能
 * @Anthor: taolin
 * @Date: 2019/2/21
 * @Version V2.0 <描述当前版本功能>
 */
public class SingleInstanceDemo {
    private static SingleInstanceDemo sSingleInstanceDemo = new SingleInstanceDemo();
    private SingleInstanceDemo(){

    }
    public synchronized static SingleInstanceDemo getInstance(){
        return sSingleInstanceDemo;
    }
}

这种写法,不能确保你的实例是在调用getInstance方法时生成的,因为类的加载机制是在可能需要使用到这个类的时候就加载(比如其他地方引用到了这个类名等等),不清楚的可以看下上篇文章 静态变量的生命周期,所以这种也不能达到懒加载的效果。

1.3 DCL写法

/**
 * @FileName: com.example.hik.lib
 * @Desription: 描述功能
 * @Anthor: taolin
 * @Date: 2019/2/21
 * @Version V2.0 <描述当前版本功能>
 */
public class SingleInstanceDemo {
    private static SingleInstanceDemo sSingleInstanceDemo;
    private SingleInstanceDemo(){

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

可以看到,把synchronized关键字是移到了内部,保证不用每次调用方法都得获取同步锁,性能有一定的提升,但是有一个问题,在Java指令中,对象的创建和赋值不是一步操作的,JVM会对代码进行一定的指令重排序(具体规则就不多介绍了,自行google),也就是说可能JVM会先直接赋值给instance成员,然后再去初始化这个sSingleInstanceDemo实例,这样就会出现问题

当然也就解决办法,加上volatile关键字就好了,可以禁止指令重排序

1.4 静态内部类写法

/**
 * @FileName: com.example.hik.lib
 * @Desription: 描述功能
 * @Anthor: taolin
 * @Date: 2019/2/21
 * @Version V2.0 <描述当前版本功能>
 */
public class SingleInstanceDemo {
    public static class InnerClass{
        private static final SingleInstanceDemo sSingleInstanceDemo = new SingleInstanceDemo();
    }
    private SingleInstanceDemo(){

    }
    public  static SingleInstanceDemo getInstance(){
        return InnerClass.sSingleInstanceDemo;
    }
}

乍一看!咦,好像和饿汉式有点像,只不过这里声明了一个私有的静态内部类,这样的区别就在于:

静态sSingleInstanceDemo对象的生成一定是在调用getInstance()方法的时候生成的,因为它是跟随着InnerClass这个类的加载而产生的,它本身是一个私有类,也保证了不会有其他的地方来调用InnerClass,这种写法比较推荐

1.5 枚举类写法

/**
 * @FileName: com.example.hik.lib
 * @Desription: 描述功能
 * @Anthor: taolin
 * @Date: 2019/2/21
 * @Version V2.0 <描述当前版本功能>
 */
public enum SingleInstanceDemo {
    INSTANCE;

    private SingleInstanceDemo() {
    }
}

单例的枚举实现在《Effective Java》中有提到,因为其功能完整、使用简洁、无偿地提供了序列化机制、在面对复杂的序列化或者反射攻击时仍然可以绝对防止多次实例化等优点,单元素的枚举类型被认为是实现Singleton的最佳方法。

但是枚举类就内存消耗是比正常类要大的,所以,看情况选择适合自己的最好

2 防止反射和反序列化

我们先来写个demo来看看,是不是反射和反序列化真的会导致单例模式的问题

package com.example.hik.lib;

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Constructor;

public class MyClass {
    public static void main(String[] args) throws IOException, ClassNotFoundException {
        //我们通过静态内部类方式,获取单例对象
        SingleInstanceDemo instance = SingleInstanceDemo.getInstance();
        //通过反射来获取一个对象
        SingleInstanceDemo instance2 = null;
        Class<SingleInstanceDemo> singleInstanceDemoClass = SingleInstanceDemo.class;
        try {
            Constructor<SingleInstanceDemo> constructor = singleInstanceDemoClass.getDeclaredConstructor(null);
            constructor.setAccessible(true);
            instance2 = constructor.newInstance();
        } catch (Exception mE) {
            mE.printStackTrace();
        }
        System.out.println("reflect obj :"+(instance==instance2));
        // 1. 把对象instance写入硬盘文件
        FileOutputStream fos = new FileOutputStream("object.txt");
        ObjectOutputStream oos = new ObjectOutputStream(fos);
        oos.writeObject(instance);
        oos.close();
        fos.close();
        // 2. 把硬盘文件上的对象读出来
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("object.txt"));
        SingleInstanceDemo instance3 = (SingleInstanceDemo) ois.readObject();
        System.out.println("Deserialize obj :"+(instance==instance3));
    }
}

run一下上面的代码可以看到

reflect obj :false
Deserialize obj :false
Process finished with exit code 0

居然都是false,也就是我们通过反射和反序列生成的对象和单例对象是不一样的,那么岂不是单例就不是单例的意义了,我们来改进一下代码

/**
 * @FileName: com.example.hik.lib
 * @Desription: 描述功能
 * @Anthor: taolin
 * @Date: 2019/2/21
 * @Version V2.0 <描述当前版本功能>
 */
public class   SingleInstanceDemo implements Serializable {
    public static class InnerClass{
        public static final SingleInstanceDemo sSingleInstanceDemo = new SingleInstanceDemo();
    }
    private SingleInstanceDemo(){
        if (null!=InnerClass.sSingleInstanceDemo){
            throw new RuntimeException("不要用反射哦");
        }
    }
    public  static SingleInstanceDemo getInstance(){
        return InnerClass.sSingleInstanceDemo;
    }
    private Object readResolve() throws ObjectStreamException {
        return InnerClass.sSingleInstanceDemo;
    }
}

解决办法:

  • 序列化单例,重写readResolve()方法
  • 在私有构造器里判断intance,如存在则抛异常(防止反射侵犯私有构造器)

再Run一下主代码,可以看到

java.lang.reflect.InvocationTargetException
	at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
	at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62)
	at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
	at java.lang.reflect.Constructor.newInstance(Constructor.java:423)
	at com.example.hik.lib.MyClass.main(MyClass.java:20)
Caused by: java.lang.RuntimeException: 不要用反射哦
	at com.example.hik.lib.SingleInstanceDemo.<init>(SingleInstanceDemo.java:19)
	... 5 more
Deserialize obj :true
Process finished with exit code 0

反射会抛出异常,而反序列化后对象也是和之前的单例是一样的,这样就大功告成了~

主要还是希望小伙伴能真正弄清楚每个单例模式的意义和不足之处,这样不管是在面试还是在日常开发中能够更好的掌握单例模式~比心❤