ICode9

精准搜索请尝试: 精确搜索
首页 > 其他分享> 文章详细

【对线面试官】面试官:小伙子,谈谈单例模式

2021-05-13 19:02:30  阅读:175  来源: 互联网

标签:面试官 java 对线 site LazyMan kexing 单例 null lazyMan


单例?

一个类只有一个对象实例,并对外提供一个获取实例的方法。一句话就能概括单例这个设计模式,真的只有这么简单吗?
单例模式分为两种方案,饿汉式懒汉式

一、饿汉式

  • 私有的构造方法
  • 只要当类加载的时候就初始化单例对象
public class Hungry {
 	private static Hungry hungry = new Hungry();
    private Hungry(){
    }
    
    public static Hungry newInstance(){
        return hungry;
    }
}

由于变量由static修饰,所以该对象由多个线程共享,并且在类加载阶段只初始化一次。

二、懒汉式

  • 私有的构造方法
  • 当需要使用实例对象时就创建
public class LazyMan {
   	private static volatile LazyMan lazyMan = null;
    private LazyMan() {
    }
 
    public static LazyMan newInstance() {
        if (lazyMan == null) {
        	lazyMan = new LazyMan(); 
        }
        return lazyMan;
    }
}

单线程情况下上面的代码不存在安全问题,但是放在多线程并发情况下呢?由于饿汉式是在类加载时初始化的对象,所以它在多线程情况下是线程安全的,但是懒汉式是对外提供方法创建对象,所以在并发情况下存在多线程同时操作共享资源的情景,下面我们假设一个场景:

  1. 线程A调用newInstance()初始化对象
  2. 线程A判空后进入if代码块,此时还没有完成实例化过程
  3. 线程B进来调用newInstance()方法,同时判空后进入if代码块
  4. 线程A执行new LazyMan()
  5. 线程B执行new LazyMan()

在这种情况下new了两次对象,破坏了单例

在多线程情况下如何保证线程安全,不用说,第一反应肯定是加锁,下面我们来加锁:

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

跑10个线程

site.kexing.lock.LazyMan@73c1f984
site.kexing.lock.LazyMan@73c1f984
site.kexing.lock.LazyMan@73c1f984
site.kexing.lock.LazyMan@73c1f984
site.kexing.lock.LazyMan@73c1f984
site.kexing.lock.LazyMan@73c1f984
site.kexing.lock.LazyMan@73c1f984
site.kexing.lock.LazyMan@73c1f984
site.kexing.lock.LazyMan@73c1f984
site.kexing.lock.LazyMan@73c1f984

很明显现在懒汉式是线程安全的,但是上文的synchronized 是直接锁的方法,这种方案锁的粒度太大,如果方法体中有大量的业务代码不需要同步,方法的性能,效率会非常低下,所以下面我们用同步代码块来降低锁的粒度

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

那么思考下这种方法在并发条件下是线程安全的吗?
答案不安全!

思考下面的场景:
1、线程A进入newInstance(),判断为空,拿到锁
2、线程B进入newInstance(),判断为空,发现锁被占有,等待
3、线程A new完对象后释放掉锁
4、线程B往下执行拿到锁,new对象

此时也是new了两个对象,也破坏了单例

测试:
同样跑10个线程

site.kexing.lock.LazyMan@37bafe8f
site.kexing.lock.LazyMan@1cd6fdf0
site.kexing.lock.LazyMan@27566eaf
site.kexing.lock.LazyMan@56b19245
site.kexing.lock.LazyMan@6b92174a
site.kexing.lock.LazyMan@5c98f75b
site.kexing.lock.LazyMan@73c1f984
site.kexing.lock.LazyMan@58ae7869
site.kexing.lock.LazyMan@4686939d
site.kexing.lock.LazyMan@1646a158

DCL懒汉式(双重检测锁)

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

为什么对象要加volatile

首先聊聊volatile有哪些特性:

  • 可见性
  • 非原子性
  • 禁止指令重排

我们来看看new LazyMan()底层到底做了什么?

  1. lazyMan 分配内存空间
  2. 调用构造方法初始化lazyMan
  3. lazyMan指向分配的内存空间,此时的lazyMan才不为null

CPU为了提高程序编译后指令的效率,往往会将指令重排,达到CPU认为最优的方案,上面的123可能会被重排为132

如果这个操作底层的指令被重排为132,思考下面场景:

  1. 线程A进入双重检测锁,执行到指令重排后的指令3(注意此时lazyMan已经指向了内存空间,不为null
  2. 线程B进来,发现lazyMan不为null,直接return
  3. 此时return的对象还没有经过指令2构造初始化,也就是一块没有填充值的内存空间

volatile关键字可以避免指令重排,始终按序执行

两层检测并加锁可有效避免线程安全问题,第一层判断主要是为了减少线程争夺资源,如果不为空后则不会去抢夺锁,降低CPU压力

真的安全吗?

别忘了Java中有一种技术叫做反射

通过反射破坏单例

public class LazyMan {
   	private static volatile LazyMan lazyMan = null;
    private LazyMan() {
    }
 
    public static LazyMan newInstance() {
        if (lazyMan == null) {
            synchronized(LazyMan.class) {
            	if(lazyMan == null){
                    lazyMan = new LazyMan();
                }
            }
        }
        return lazyMan;
    }
 	public static void main(String[] args) throws Exception {
        //正常调用方法创建
        LazyMan lazyMan1 = LazyMan.newInstance();
        //拿到class
        Class<LazyMan> lazyManClass = LazyMan.class;
        //拿到构造器
        Constructor<LazyMan> declaredConstructor = lazyManClass.getDeclaredConstructor(null);
        //反射构造器创建
        LazyMan lazyMan = declaredConstructor.newInstance();
        System.out.println(lazyMan);
        System.out.println(lazyMan1);
    }
}

编译运行:

site.kexing.single.LazyMan@b4c966a
site.kexing.single.LazyMan@2f4d3709

通过反射拿到构造器创建了两个实例对象

解决方案,信号量法:

这种方法不论是通过反射还是调用提供的方法只能构造出一个实例!

public class LazyMan {
	private static Boolean kexing = false;
   	private static volatile LazyMan lazyMan = null;
    
    private LazyMan() {
    	synchronized (LazyMan.class){
            if(kexing == false){
                kexing = true;
            }else {
                throw new RuntimeException("请不要试图使用反射破坏单例");
            }
        }
    }
 
 	//双重检测锁
    public static LazyMan newInstance() {
        if (lazyMan == null) {
            synchronized(LazyMan.class) {
            	if(lazyMan == null){
                    lazyMan = new LazyMan();
                }
            }
        }
        return lazyMan;
    }
}

只要是第一次创建实例,信号量kexing 置为true,随后进来的都会走else抛出异常

同样的,执行上面同样的main方法进行测试:

Exception in thread "main" java.lang.reflect.InvocationTargetException
	at java.base/jdk.internal.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
	at java.base/jdk.internal.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62)
	at java.base/jdk.internal.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
	at java.base/java.lang.reflect.Constructor.newInstanceWithCaller(Constructor.java:500)
	at java.base/java.lang.reflect.Constructor.newInstance(Constructor.java:481)
	at site.kexing.single.LazyMan.main(LazyMan.java:54)
Caused by: java.lang.RuntimeException: 请不要试图使用反射破坏单例
	at site.kexing.single.LazyMan.<init>(LazyMan.java:17)
	... 6 more

nice,有效的阻止了重复创建实例

真的安全吗??

如果狂徒张三通过不正当手段知道了程序是通过这个方案保证单例,那么这个方案也会变得不堪一击,要知道,一个类在反射面前是光着身子的!

如何破解上文的信号量法?
很简单,拿到字节码class对象后通过getDeclaredFields()拿到类的变量域对象数组,遍历一下,field.getName()拿到成员变量名,field.get(field.getName())拿到成员变量的值,通过set(Object obj, Object value)修改,始终保证信号量为true,狂徒张三:就这?

枚举单例(终极方案)

public enum  EnumLazyMan{
    INSTANCE;

    public static EnumLazyMan newInstance() {
        return INSTANCE;
    }
}

狂徒张三:反射试试?

Exception in thread "main" java.lang.IllegalArgumentException: Cannot reflectively create enum objects
	at java.base/java.lang.reflect.Constructor.newInstanceWithCaller(Constructor.java:493)
	at java.base/java.lang.reflect.Constructor.newInstance(Constructor.java:481)
	at site.kexing.single.EnumLazyMan.main(EnumLazyMan.java:29)

Cannot reflectively create enum objects

张三:…

我们点进源码看看为什么会这样

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;
    }

抽出重点:

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

在Java圣经《Effective Java》中,Joshua Bloch这么说:

A single-element enum type is often the best way to implement a singleton.
枚举是一般情况下最好的Java实现单例的方法

It is more concise, provides the serialization machinery for free, and provides an ironclad guarantee against multiple instantiation, even in the face of sophisticated serialization or reflection attacks.
枚举单例可以有效防御两种破坏单例(使单例产生多个实例)的行为:反射攻击与序列化攻击

标签:面试官,java,对线,site,LazyMan,kexing,单例,null,lazyMan
来源: https://blog.csdn.net/qq_43442335/article/details/116750662

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有