秃头系列-面试篇之单例模式

129 阅读5分钟

「这是我参与2022首次更文挑战的第13天,活动详情查看:2022首次更文挑战

前言

  • 关于作者:励志不秃头的一个CURD的Java农民工,想挑战看看自己能完成多少天的更文挑战
  • 关于文章:以下内容单纯为作者了解的,如有不对,欢迎各路大神指导,下面聊聊单例模式,我还遇到过面试时要求手写单例模式,所以还是有必要了解各种写法的。马上就金三银四了,快来看看吧。

单例模式

单例模式,Singleton Pattern;是指一个类在任何情况下都绝对只有一个实例,并提供了一个全局访问带你。

单例模式是一个创建型模式

饿汉式单例

是指类加载的时候立即初始化,并且创建单例对象,是绝对线程安全的。因为在线程还没出现以前就已经实例化了

饿汉式适用在单例对象比较少的情况

public class HungrySingleton {

    //写法一:
//    private static HungrySingleton hungrySingleton = new HungrySingleton();
//    private HungrySingleton(){}



    //写法二,静态代码机制
    private static HungrySingleton hungrySingleton ;
    static {
        hungrySingleton = new HungrySingleton();
    }


    public static HungrySingleton getInstance(){
        return hungrySingleton;
    }
    
}

懒汉式单例

  • 最终版
public class LazySingleton {
    private volatile static LazySingleton lazySingleton = null;

    private LazySingleton(){}

    public static LazySingleton getInstance(){
        if(lazySingleton == null){
            synchronized (LazySingleton.class){
                if(lazySingleton == null){
                    lazySingleton = new LazySingleton();
                }
            }
        }
        return lazySingleton;
    }
}
  1. 为了解决线程安全,使用了 synchronized 关键字;不在 getInstance( ) 方法上加上 synchronized , 是因为在方法上加锁,在线程数量比较多情况下,如果 CPU 分配压力上升,会导致大批量线程出现阻塞,从而导致程序运行性能大幅下降
  2. 使用 volatile 关键字修饰,是为了避免JVM底层有可能发生的重排序
  • 静态内部类写法
//兼顾了饿汉式的内存浪费以及synchronized性能的问题
public class LazyInnerClassSingleton {

    //默认使用 LazyInnerClassSingleton 的时候,会先初始化内部类
    //但是如果没使用的话,内部类是不加载的
    private LazyInnerClassSingleton(){
        //为了不被反射破坏单例
        if(LazyHolder.LAZY != null){
             throw new RuntimeException("非法破坏单例");
        }

    }

    //final 关键字 ,保证该方法不会被重写、重载
    //static 为了使单例的空间共享
    public final static LazyInnerClassSingleton getInstance(){
        //在返回结果前,一定会先加载内部类
        return LazyHolder.LAZY;
    }

    //默认不加载
    private static class LazyHolder{
        private static final LazyInnerClassSingleton LAZY = new LazyInnerClassSingleton();
    }
}

反射破坏单例

测试代码:

public class InstanceTest {
    public static void main(String[] args) {
        //破坏静态内部类
        try{
            Class<?> clazz = LazyInnerClassSingleton.class;
            //通过反射拿到私有的构造方法
            Constructor c = clazz.getDeclaredConstructor(null);
            //强制访问
            c.setAccessible(true);
            //暴力初始化
            Object object1 = c.newInstance();
            Object object2 = c.newInstance();
            System.out.println(object1 == object2);
        }catch (Exception e){
            e.printStackTrace();
        }

        //破坏懒汉式
        try{
            Class<?> clazz = LazySingleton.class;
            Constructor c = clazz.getDeclaredConstructor(null);
            c.setAccessible(true);
            Object object1 = c.newInstance();
            Object object2 = c.newInstance();
            System.out.println(object1 == object2);
        }catch (Exception e){
            e.printStackTrace();
        }
    }
}
  • 破坏静态内部类:当静态内部类的构造方法里没有加上如下判断,便会被反射破坏单例
if(LazyHolder.LAZY != null){
    throw new RuntimeException("非法破坏单例");
}
  • 破坏懒汉式:避免方式:
public class LazySingleton {
    private volatile static LazySingleton lazySingleton = null;
    private static boolean flag = false;

    private LazySingleton(){
        synchronized (LazySingleton.class){
            if(flag == false){
                flag = true;
            }else {
                throw new RuntimeException("非法破坏单例");

            }

        }

    }

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

序列化破坏单例

public class SeriableSingleton implements Serializable {

    //序列化就是说把内存中的状态通过转换成字节码的形式
    //从而转换一个IO流,写入到其他地方(可以是磁盘、网络IO)
    //内存中状态给永久保存下来了

    //反序列化
    //讲已经持久化的字节码内容,转换为IO流
    //通过IO流的读取,进而将读取的内容转换为Java对象
    //在转换过程中会重新创建对象new

    public  final static SeriableSingleton INSTANCE = new SeriableSingleton();
    private SeriableSingleton(){}

    public static SeriableSingleton getInstance(){
        return INSTANCE;
    }

    

}

测试代码:

public class SeriableSingletonTest {
    public static void main(String[] args) {

        SeriableSingleton s1 = null;
        SeriableSingleton s2 = SeriableSingleton.getInstance();

        FileOutputStream fos = null;
        try {
            fos = new FileOutputStream("SeriableSingleton.obj");
            ObjectOutputStream oos = new ObjectOutputStream(fos);
            oos.writeObject(s2);
            oos.flush();
            oos.close();


            FileInputStream fis = new FileInputStream("SeriableSingleton.obj");
            ObjectInputStream ois = new ObjectInputStream(fis);
            s1 = (SeriableSingleton)ois.readObject();
            ois.close();

            System.out.println(s1);
            System.out.println(s2);
            System.out.println(s1 == s2);

        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

得到的结果可以看出:s1和s2是两个不同的对象,违背了单例的设计原则

修改方法,只需要增加readResolve() 方法:

private  Object readResolve(){
    return  INSTANCE;
}

这是因为JDK在底层源码设计的时候进行了避免,从源码可以看出:在 invokeReadResolve()方法中用反射调用了 readResolveMethod 方法。

虽然,增加 readResolve()方法返回实例,解决了单例被破坏的问题。但是,我们通过分析源码以及调试,我们可以看到实际上实例化了两次,只不过新创建的对象没有被返回而已

注册式单例

枚举写法

public enum EnumSingleton {
    //枚举单例
    INSTANCE;

    private Object data;

    public Object getData(){
        return data;
    }

    public static EnumSingleton getInstance(){
        return INSTANCE;
    }
}
  • 枚举式单例是饿汉式单例,在静态代码代码块就对 INSTANCE 进行了赋值
  • 序列化并不能破坏枚举式单例,因为在 JDK源码 中的 readEnum () 方法,通过类名和Class对象类找到一个唯一的枚举对象,所以枚举对象不可能被类加载器加载多次
  • 反射不能破坏枚举式单例,会抛出异常: java.lang.NoSuchMethodException,在JDK的源码中,Constructor 的 newInstance()方法中:

    

在 newInstance()方法中做了强制性的判断,如果修饰符是 Modifier.ENUM 枚举类型,直接抛出异常。

容器写法

容器写法适用于创建实例非常多的情况,便于管理,spring就是使用容器写法管理bean,非线程安全的

public class ContainerSingleton {
    private ContainerSingleton(){}

    private static Map<String,Object> ioc = new ConcurrentHashMap<>();

    public static Object getBean(String beanName){
        //保证线程安全,ConcurrentHashMap只是保证了map的线程安全
        synchronized (ioc){
            if(!ioc.containsKey(beanName)){
                Object object = null;
                try{
                    object = Class.forName(beanName).newInstance();
                    ioc.put(beanName, object);
                } catch (Exception e) {
                    e.printStackTrace();
                } 
                return object;
            }else {
                return ioc.get(beanName);
            }
        }
    }
}

ThreadLocal 线程单例

  • ThreadLocal 不保证全局唯一,但是保证了在单个线程中是唯一的,天生的线程安全
  • ThreadLocal 将所有的对象都放在了ThreadLocalMap 里,线程作为key,是以空间换时间来实现线程间的隔离的
public class ThreadLocalSingleton {
    private static final ThreadLocal<ThreadLocalSingleton> threadLocalInstance = 
            new ThreadLocal<ThreadLocalSingleton>(){
                @Override
                protected ThreadLocalSingleton initialValue() {
                    return new ThreadLocalSingleton();
                }
            };
    
    private ThreadLocalSingleton(){
        
    }
    
    public static  ThreadLocalSingleton getInstance(){
        return threadLocalInstance.get();
    }
}

好了,以上就是单例模式相关的几种写法,希望对大家有一定的认识和帮助,我是新生代农民工L_Denny,我们下篇文章见。