【面试】一步步教你玩转单例模式


前言

设计模式在面试中是面试官常常问到的几个经典问题之一,其中单例模式又是重灾区,你可能会想,单例模式谁不会啊;不就是:单例模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。对的,这就是单例模式的定义,但是单例模式在多线程下就真的安全吗?如果破坏了单例模式该怎么解决呢?带着疑问下面一步步破解这些问题!

一、单例模式—“饿汉式”

单例模式(立即加载),又称为“饿汉式”,顾名思义,饿汉见到实物就迫不及待的想要进食,非常急迫;该形式的单例模式也如其名。
单例模式—“饿汉式”是指在调用方法前,就将对象创建好。下面用一段代码来进行说明:

// 单例模式---“饿汉式”
public class HungryMan {
// 可能会浪费空间
    private byte[] data1 = new byte[1024*1024];
    private byte[] data2 = new byte[1024*1024];
    private byte[] data3 = new byte[1024*1024];
    private byte[] data4 = new byte[1024*1024];
    private HungryMan(){
    }
    private final static HungryMan Hungryman = new HungryMan();
    public static HungryMan getInstance(){
      return Hungryman;
    }
}

以上的代码是一个经典的“饿汉式”单例模式,不难看出,以上的代码存在着一些问题:

  • 还没调用方法,对象已经创建,并且创建了一堆数组,但是没有使用,存在着大量内存空间的浪费;
  • 因为getInstance()是非同步方法,所以上述代码不能有其他实例变量,换句话说,该代码可能会存在非线程安全问题;

二、单例模式—“懒汉式”

1.普通单例模式

单例模式—“懒汉式”,根据字面意思说明该模式非常懒,不到最后关键时刻不做事,对于单例模式而言,就是不到调用它的时候不加载,只有调用它时才会创建对象,接下来用一段代码说明这个模式:

// 单例模式---“懒汉式”
public class LazyMan{
    private LazyMan(){
    }
    private static LazyMan lazyMan;
    public static LazyMan getInstance(){
        if (lazyMan==null){
          lazyMan = new LazyMan(); 
        }
        return lazyMan;
    }
}

上述代码在单线程模式下是没有任何问题的,但是当使用到多线程并发中,问题便显现出来了,读者可自测多线程情况下出现的问题。该怎么解决呢?

2.“DCL懒汉式”—双重检测锁模式

说到多线程并发问题的解决方案,大家第一时间就会想到一个关键字—synchronized。若是我们在上面的代码中加上synchronized关键字能否处理单例模式在多线程并发下的安全问题呢?说干就干:

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

可以看出,上述代码在普通的单例模式下加上了synchronized关键字,将 LazyMan.class 作为锁对象进行锁定。此时在多线程并发条件下进行测试,经测试,多线程并发的安全问题解决。这就是面试官最喜欢问的“DCL懒汉式”模式,又称双重检测锁模式!

3.完整的DCL懒汉式”—双重检测锁模式

DCL懒汉式”—双重检测锁模式就解决了单例模式在多线程并发性的安全性问题嘛?要是这样话,这篇文章也就没有存在的必要了。下面,好戏才刚刚开场;
请仔细研究上述代码,不难发现 lazyMan = new LazyMan() 竟不是一个原子性操作,也就是说,在某些极端条件下,还是会出问题的。有jvm的基础的同学可能会发现,虽然这只是一条执行语句,但是在底层实现时,是分了三步完成的:

1. 为lazyMan分配内存空间;
2. 执行构造方法,初始化lazyMan;
3. 将lazyMan指向分配的内存地址。

但是由于 JVM 具有指令重排的特性,执⾏顺序有可能变成 1->3->2。指令重排在单线程环境下不会出现问题,但是在多线程环境下会导致⼀个线程获得还没有初始化的实例。例如,线程 T1 执⾏了 1 和 3,此时 T2 调⽤ getInstance() 后发现 lazyMan 不为空,因此返回lazyMan ,但此时 lazyMan 还未被初始化。该怎样保证 jvm 是按照顺序执行呢?对,volatile,这个关键字不仅可以保证可见性,还可以禁止指令重排,修改代码:

public class LazyMan{
    private LazyMan(){
    }
    private volatile static LazyMan lazyMan;
    public static LazyMan getInstance(){
    	if (lazyMan==null){
          	synchronized(LazyMan.class){
        		if (lazyMan==null){
          			lazyMan = new LazyMan(); 
        		}
       		 }
        }
        return lazyMan;
    }
}

可以看出,上述代码已经加入了 volatile 关键字,这才是完整的DCL懒汉式”—双重检测锁模式。

4.静态内部类实现单例模式

其实,面试的时候说到上面就已经算不错,但是对于一个对代码充满好奇心的人,对于想进大厂的人来说还远远不够,接下来便是炫技时刻!学过Java的人都知道有个类叫做静态内部类,这个类也可以实现单例模式嘛?接着往下看:

public class LazyMan{
    private LazyMan(){
    }
    public static LazyMan getInstance(){
        return InnerClass.lazyMan;
    }
    public static class InnerClass{
		private static final LazyMan lazyMan = new LazyMan(); 
	}
}

这便是静态内部类实现单例模式,这种方式没什么实际作用,就是展现自己的技术。

5.反射破坏单例

但是!!!这就结束了嘛?并没有,学过Java的同学一定学过一项非常霸道的技术—反射,只要有反射的存在,任何 private 关键词都是纸老虎,在反射面前形同虚设。下面将用反射进行形如“黑客”的攻防!

public class LazyMan{
    private LazyMan(){
    }
    private volatile static LazyMan lazyMan;
    public static LazyMan getInstance(){
    	if (lazyMan==null){
          	synchronized(LazyMan.class){
        		if (lazyMan==null){
          			lazyMan = new LazyMan(); 
        		}
       		 }
        }
        return lazyMan;
    }
    
    public static void main(String[] args) throws Exception {
   		LazyMan instance = LazyMan.getInstance();
   		Constructor<LazyMan> declaredConstructor = LazyMan.class.getDeclaredConstructor(null);
		declaredConstructor.setAccessible(true);//无视私有构造器
		LazyMan instance2 = declaredConstructor.newInstance();
		System.out.println(instance);
		System.out.println(instance2);
    }
}

运行代码,结果:
f
进行测试,可以发现,两个对象竟然不一样,也就是说使用反射可以破解由 volatilesychronized 关键字修饰过的单例模式,由此可见,反射真的很霸道。

6.三重检测锁模式

可以看到上面的代码使用反射破解了单例模式,它是如何破解的呢?我们该如何解决这个问题呢?
看代码,反射是通过得到构造器破解的单例模式,那我们若是在构造器中加入判断能不能解决这个问题呢?

public class LazyMan{
    private LazyMan(){
    	synchronized(LazyMan.class){
    		if(lazyMan!=null){
    		throw new RuntimeException("已有对象,请不要搞破坏!");
    		}
    	}
    }
    private volatile static LazyMan lazyMan;
    public static LazyMan getInstance(){
    	if (lazyMan==null){
          	synchronized(LazyMan.class){
        		if (lazyMan==null){
          			lazyMan = new LazyMan(); 
        		}
       		 }
        }
        return lazyMan;
    }
    
    public static void main(String[] args) throws Exception {
   		LazyMan instance = LazyMan.getInstance();
   		Constructor<LazyMan> declaredConstructor = LazyMan.class.getDeclaredConstructor(null);
		declaredConstructor.setAccessible(true);//无视私有构造器
		LazyMan instance2 = declaredConstructor.newInstance();
		System.out.println(instance);
		System.out.println(instance2);
    }
}

运行代码,结果:
在这里插入图片描述可以看出,在构造器中加入判断,可以解决此类问题的发生,这种方式被称为三重检测锁方式。

7.构造器破坏单例模式

你以为这就结束了?有没有想过一个问题,若是我两个对象都是通过构造器实现的呢?

public class LazyMan{
    private LazyMan(){
        synchronized(LazyMan.class){
            if(lazyMan!=null){
                throw new RuntimeException("已有对象,请不要搞破坏!");
            }
        }
    }
    private volatile static LazyMan lazyMan;
    public static LazyMan getInstance(){
        if (lazyMan==null){
            synchronized(LazyMan.class){
                if (lazyMan==null){
                    lazyMan = new LazyMan();
                }
            }
        }
        return lazyMan;
    }

    public static void main(String[] args) throws Exception {
        //LazyMan instance = LazyMan.getInstance();
        Constructor<LazyMan> declaredConstructor = LazyMan.class.getDeclaredConstructor(null);
        declaredConstructor.setAccessible(true);//无视私有构造器
        LazyMan instance = declaredConstructor.newInstance();
        LazyMan instance2 = declaredConstructor.newInstance();
        System.out.println(instance);
        System.out.println(instance2);
    }
}

在这里插入图片描述
不言而喻,我将得到两个对象,不好意思,我又破解了你的三重检测锁模式。

8.使用标志位解决构造器问题

上面的代码通过构造器拿到了两个不同的对象,那若是想解决这个问题,势必在构造器中再加一层逻辑,当已经有对象的时候就不能再创建对象了,该怎么实现这个逻辑呢?这里可以用一个标志位,每当构造方法被调用的时候,便将标志位进行一次改变,实现对构造器方法的保护。

public class LazyMan{

    private static boolean flag=false;
    private LazyMan(){
        synchronized(LazyMan.class){
            if (flag==false){
                flag=true;
            }else{
                throw new RuntimeException("已有对象,请不要搞破坏!");
            }
        }
    }
    private volatile static LazyMan lazyMan;
    public static LazyMan getInstance(){
        if (lazyMan==null){
            synchronized(LazyMan.class){
                if (lazyMan==null){
                    lazyMan = new LazyMan();
                }
            }
        }
        return lazyMan;
    }

    public static void main(String[] args) throws Exception {
        //LazyMan instance = LazyMan.getInstance();
        Constructor<LazyMan> declaredConstructor = LazyMan.class.getDeclaredConstructor(null);
        declaredConstructor.setAccessible(true);//无视私有构造器
        LazyMan instance = declaredConstructor.newInstance();
        LazyMan instance2 = declaredConstructor.newInstance();
        System.out.println(instance);
        System.out.println(instance2);
    }
}

运行代码:
在这里插入图片描述
可以看出,在加上一个标志位的情况下,可以解决由构造器方法触发的问题。

9.修改标志位破坏单例模式

你以为加入了标志位就可以保证安全吗?我要是想要破坏单例模式,修改你的标志位不就可以了嘛,创建一个对象修改一次标志位,这样,我还是可以破坏你的单例模式。桀桀桀桀~

public class LazyMan{

    private static boolean flag=false;
    private LazyMan(){
        synchronized(LazyMan.class){
            if (flag==false){
                flag=true;
            }else{
                throw new RuntimeException("已有对象,请不要搞破坏!");
            }
        }
    }
    private volatile static LazyMan lazyMan;
    public static LazyMan getInstance(){
        if (lazyMan==null){
            synchronized(LazyMan.class){
                if (lazyMan==null){
                    lazyMan = new LazyMan();
                }
            }
        }
        return lazyMan;
    }

    public static void main(String[] args) throws Exception {
        //LazyMan instance = LazyMan.getInstance();

        Field flag = LazyMan.class.getDeclaredField("flag");
        flag.setAccessible(true);

        Constructor<LazyMan> declaredConstructor = LazyMan.class.getDeclaredConstructor(null);
        declaredConstructor.setAccessible(true);//无视私有构造器
        LazyMan instance = declaredConstructor.newInstance();

        flag.set(instance,false);

        LazyMan instance2 = declaredConstructor.newInstance();
        System.out.println(instance);
        System.out.println(instance2);
    }
}

在创建对象之前,先获取标志位 flag 并将权限修改,创建一个对象之后,再将标志位置为 false,如此一来,仍然可以继续创建对象,运行代码:
这便是静态内部类实现单例模式,这种方式没什么实际作用,就是展现自己的技术。
可以看出,通过反射获取标志位,仍然可以创建两个对象,破坏了单例模式。

三、单例模式—枚举

既然反射模式如此强大,该怎样解决反射带来的破坏性呢?那我们只能从源码中进行分析该怎么解决这个问题。

@CallerSensitive
    @ForceInline // to ensure Reflection.getCallerClass optimization
    public T newInstance(Object ... initargs)
        throws InstantiationException, IllegalAccessException,
               IllegalArgumentException, InvocationTargetException
    {
        Class<?> caller = override ? null : Reflection.getCallerClass();
        return newInstanceWithCaller(initargs, !override, caller);
    }

    /* package-private */
    T newInstanceWithCaller(Object[] args, boolean checkAccess, Class<?> caller)
        throws InstantiationException, IllegalAccessException,
               InvocationTargetException
    {
        if (checkAccess)
            checkAccess(caller, clazz, clazz, modifiers);

        if ((clazz.getModifiers() & Modifier.ENUM) != 0)
            throw new IllegalArgumentException("Cannot reflectively create enum objects");

        ConstructorAccessor ca = constructorAccessor;   // read volatile
        if (ca == null) {
            ca = acquireConstructorAccessor();
        }
        @SuppressWarnings("unchecked")
        T inst = (T) ca.newInstance(args);
        return inst;
    }

可以看到有这么一句话:***Cannot reflectively create enum objects***不能反射地创建枚举对象
既然如此,那我们可以用枚举来保证单例模式啊。
那我们接下来写一个枚举类尝试一下:

public enum EnumSingle {
    INSTANCE;

    public EnumSingle getInstance() {
        return INSTANCE;
    }
}

class Test {
    public static void main(String[] args) throws NoSuchMethodException,
            IllegalAccessException, InvocationTargetException, InstantiationException {
        EnumSingle instance1 = EnumSingle.INSTANCE;
        Constructor<EnumSingle> declaredConstructor =
                EnumSingle.class.getDeclaredConstructor();
        declaredConstructor.setAccessible(true);
        EnumSingle instance2 = declaredConstructor.newInstance();
        System.out.println(instance1);
        System.out.println(instance2);
    }
}

以为可以很好的解决问题,但是发现,出大问题了:
在这里插入图片描述
上述代码完全是按照枚举的规范所写,但是怎么会出问题呢?问题显示,没有无参构造。开始排除bug,首先进行源码分析,将代码进行反编译:

反编译的代码仍然显示有一个空参构造,说明反编译的代码还是没能找到问题的根源,继续深入,官方工具没用的话,就尝试其他工具,在此使用 jad 反编译成java文件:
在这里插入图片描述
通过一系列编译,终于找到问题所在,原来枚举类的构造方法并不是我们所看的空参构造,而是有两个参数,知道了问题就很好解决,将代中的构造器加上参数再次进行尝试:

public enum EnumSingle {
    INSTANCE;

    public EnumSingle getInstance() {
        return INSTANCE;
    }
}

class Test {
    public static void main(String[] args) throws NoSuchMethodException,
            IllegalAccessException, InvocationTargetException, InstantiationException {
        EnumSingle instance1 = EnumSingle.INSTANCE;
        Constructor<EnumSingle> declaredConstructor =
                EnumSingle.class.getDeclaredConstructor(String.class, int.class);
        declaredConstructor.setAccessible(true);
        EnumSingle instance2 = declaredConstructor.newInstance();
        System.out.println(instance1);
        System.out.println(instance2);
    }
}

运行代码:
在这里插入图片描述
可以看出,得到了我们想要的结果,至此,枚举类与单例模式研究完毕!


总结

单例模式在多线程并发下,并不是安全的,道高一尺魔高一丈,反射可以破坏所谓的单例模式。此时,枚举登场,但是要注意,枚举没有无参构造,而是有参构造。经过上面的学习,彻底玩转单例模式!

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值