转载

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

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

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
复制代码

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

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

原文  https://juejin.im/post/5c921af5f265da60cb376da3
正文到此结束
Loading...