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

前言

说到单例设计模式,你们应该都比较熟悉,也能说个一二三,单例单例,无非就是 保证一个类只有一个对象实例嘛,通常就是私有化构造函数,而后再暴露一个方法提供一个实例,确实没错,可是怎么样保证一个单例的安全性呢,私有构造函数,那若是反射强势调用呢?再好比序列化一个对象后,反序列化呢?生成的对象是否仍是同样的?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方法时生成的,由于类的加载机制是在可能须要使用到这个类的时候就加载(好比其余地方引用到了这个类名等等),不清楚的能够看下上篇文章 静态变量的生命周期,因此这种也不能达到懒加载的效果。bash

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关键字就行了,能够禁止指令重排序post

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,这种写法比较推荐ui

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的最佳方法。google

可是枚举类就内存消耗是比正常类要大的,因此,看状况选择适合本身的最好

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

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

主要仍是但愿小伙伴能真正弄清楚每一个单例模式的意义和不足之处,这样不论是在面试仍是在平常开发中可以更好的掌握单例模式~比心❤

相关文章
相关标签/搜索