java之单例模式分析(面试必用)

大家面试的免不了会让你写个单例模式,那么写就写呗:

 

public class SingletonObject1 {

    /**
     * can't lazy load.
     */
    private static final SingletonObject1 instance = new SingletonObject1();

    private SingletonObject1() {
        //empty
    }

    public static SingletonObject1 getInstance() {
        return instance;
    }
}

 

好了面试官就会问你有没有什么要补充的啊?你说没了,那么恭喜你,这次面试凉凉喽.

我们分析撒,这样写虽然一点毛病没有但是你们发现没有它不能懒加载,也就去是说这是恶汉试的方式,由于是被静态代码块修饰它加载的时候会随着类加载,也就是说主动加载的方式那么它有什么问题呢?如果我们很长时间不使用它就会占用内存哦!!

懒加载的方式:

 

public class SingletonObject2 {

    private static SingletonObject2 instance;

    private SingletonObject2() {
        //empty
    }

    public static SingletonObject2 getInstance() {
        if (null == instance)
            instance = new SingletonObject2();

        return SingletonObject2.instance;
    }
}

ok的我们这是一个懒加载的单例模式,但是问题又来了我们知道在多线程的环境下它是不安全的,有可能抢线程的时候会创建多个线程,问题来了怎么解决呢?当然是加锁喽:

 

public class SingletonObject3 {
    private static SingletonObject3 instance;

    private SingletonObject3() {
        //empty
    }

    public synchronized static SingletonObject3 getInstance() {

        if (null == instance)
            instance = new SingletonObject3();

        return SingletonObject3.instance;
    }
}

我们看到前面的问题,貌似都解决了,但是还不够,我们这其中加了锁哦,会变成串行话的 也就是性能变差了,唉哪儿来这么多问题真是的解决:double check的方式:

也就是检查了2次我们以Netty ConstantPool中的 一段代码为例子解释一下 

  private T getOrCreate(String name) {
        T constant = constants.get(name);
        if (constant == null) {
            final T tempConstant = newConstant(nextId(), name);
            //如果没有的话那么就赋值
            constant = constants.putIfAbsent(name, tempConstant);
            if (constant == null) {
                return tempConstant;
            }
        }

        return constant;
    }
 

 

 

也就说要返回Contant实例,对它进行了两次判定,阅读顺序从1 到5 依次执行查看,并且最后返回了该实例

我的例子:

public class SingletonObject4 {

    private static SingletonObject4 instance;

    private SingletonObject4() {
        //---
    }

    //double check
    public static SingletonObject4 getInstance() {

        if (null == instance) {
            synchronized (SingletonObject4.class) {
                if (null == instance)
                    instance = new SingletonObject4();
            }
        }

        return SingletonObject4.instance;
    }
}

这样的话,我们加了个class锁,这样跟上面比有什么区别呢?

很显然嘛!!上面那个是不管什么情况下都会加锁,而下面这种doublecheck方式呢?加锁的情况只发生在线程争抢的情况下发现木有?

好了到现在我们解决了不能懒加载的问题的问题,又解决了在多线程的情况下可能产生多个实例的问题,加锁后解决了性能问题,你可能绝的肯定完美了,但是....我们还有问题,看着代码你大脑回路想一想会不会可能出现Null呢?

你肯定觉得我是在找茬,但是找茬的不是我可能是面试官哦(我就被这样问到过).....

其实这就涉及到底层的问题也就是构造过程中的重排序,

我想说的就是,单一个线程去拿单例的时候,可能这个类里面的很多构造方法没有构造完,也就是说没有初始化完成但是这个线程却拿走了使用了用到里面的参数的时候,发现没有构造出来,所以也就空指针啦!

为什么会出现这种情况,因为java在编译的过程中会进行优化重排序,等等,因为java有jrt,等用来提高性能,

编译阶段和运行阶段都会进行优化,

举个简单的栗子:

我们写了两个变量会问jvm在执行的时候肯定会执行int i=0吧?

但是不一定哦,有可能会先执行int j=10;

我说的是执行的过程,jvm之保证程序正常运行即可,也就是说参数正确即可,这个过程中jvm会进行一些编译器的优化,还有运行时的优化

好的既然问题来了,那么只是问题吧:

 

public class SingletonObject5 {

    private static volatile SingletonObject5 instance;

    private SingletonObject5() {
        //
    }

    //double check add volatile
    public static SingletonObject5 getInstance() {

        if (null == instance) {
            synchronized (SingletonObject4.class) {
                if (null == instance)
                    instance = new SingletonObject5();
            }
        }
        return SingletonObject5.instance;
    }
}

我们加了个volatile,自己去百度volatile关键字的特性啦!!!  这个太暴力了,不让人jvm重排序不推荐

还有一种先看图 

 

JVM呢在内的初始化阶段,也就是class被加载后,被线程使用之前,类的初始化阶段,在这个阶段会执行类的初始化,那在执行类的初始化期间。JVM会去获取一个锁,这个锁呢可以同步多个线程,对一个类的初始化. 
  也就是绿色的部分,基于这个特性,我们可以实现基于静态内部类的方式并且是线程安全的延时方案。
  那我们看一下这个图,还是线程0和线程1,蓝色和红色,那在这种实现模式中呢,对于右侧的2和3,也就是橙色的框,这两个步骤的重排序,对于前面我们讲的线程并不会看到,也就是说非构造线程,是不允许看到这个重排序的,因为之前我们讲的是线程0构造这个单例对象.初始化一个类,执行这个类的静态初始化,还有初始化在这个类中声明的静态变量。 
  
  根据Java语言规范主要分为5种情况 1首次发生的时候呢,一个类将被立刻初始化。这里所说的类呢是泛指包括接口interface也是一个类,那假设呢这个是a,那现在呢,我们说一下这几种情况都会导致这个a类被立刻初始化,
  1有一个a类型的实例已被创建
  第2种,a类中声明的一个静态方法被调用;
  第3种情况是a类中声明的一个静态成员被赋值; 
  第4种情况生命的一个静态成员被使用。并且这个成员不是一个常量成员; 前4种呢我们实际工作中啊用到的比较多, 第5种呢用到的比较少,也就是说a类,如果是一个顶级类,关于顶级类呢;
  在Java语言规范里面也有介绍。 在这个类中有嵌套的断言语句,这种情况呢a类也会被立刻初始化,也就是说刚刚说的5种情况,
  前4种呢是我们经常能碰到的,只要首次发生以上说的某一个情况这个类都会被立刻初始化,
  那我们看一下这个图,当线程0和线程一试图呢来获取这个锁的时候,也就是说获得class对象的初始化所,这个时候呢肯定只有一个线程能获得
  这个所假设线程0获得这个锁了,线程0执行静态内部类的一个初始化,对于静态内部类即使23之间存在重排序,但是线程一,是无法看到这个重排序的。因为这里边有一个class对象的初始化锁;
  因为这里边有锁,对于线程0而言,初始化这个静态内部类的时候,也就是说把这个instancenew出来,可以看到上面的我们还有紫色大框,所以呢23怎么排序无所谓,线程一看不到,因为线程一,现在在绿色区域等待,静态内部类就是基于类初始化的延迟加载解决方案 也就是我们下面的方法

public class SingletonObject6 {

   
    //static 只会被初始化一次, 同时也只能会顺序执行 被修饰的主动加载,只有使用它才会被加载
    //初始化,构建,是线程友好的不会被初始化两次
    private static class InstanceHolder {
        private final static SingletonObject6 instance = new SingletonObject6();
    }

    public static SingletonObject6 getInstance() {
        return InstanceHolder.instance;
    } 

    private SingletonObject6() {
        //构造私有化
        
    }
}

第三种,使用枚举,线程安全只会被装载一次

public class SingletonObject7 {
    private SingletonObject7() {

    }

    private enum Singleton {
        INSTANCE;//定义枚举的时候构造函数已近被创建

        private final SingletonObject7 instance;
        //只会被装载一次
        Singleton() {
            instance = new SingletonObject7();
        }
        //实例函数
        public SingletonObject7 getInstance() {
            return instance;
        }
    }

    public static SingletonObject7 getInstance() {
        return Singleton.INSTANCE.getInstance();
    }

    public static void main(String[] args) {
        //简单的测试一下下:
        IntStream.rangeClosed(1, 100)
                .forEach(i -> new Thread(String.valueOf(i)) {
                    @Override
                    public void run() {
                        System.out.println(SingletonObject7.getInstance());
                    }
                }.start());
    }
}

但是但是还有问题,因为我们可以通过序列化来破坏这个单例 !! 我们设计一个 测试   

//首先我们的让这个实例 实现序列化接口  
SingletonObject1 instance = SingletonObject1.getInstance();
//输入输出流就不要在说了吧
  ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("singleton_file"));
  oos.writeObject(instance);

 File file = new File("singleton_file");
 ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file));
//反序列化出来
 SingletonObject1 newInstance = (SingletonObject1) ois.readObject();
System.out.println(instance);
 System.out.println(newInstance);
System.out.println(instance == newInstance);

结果 不是同一个对象 !!   那为什么不是同一个对象呢? 看这个gif图留意我打断点的地方,每一步都会打断点,并且在下面说明

 

1 个raid0()这个方法.这个就是读取对象

2 第二个断点处的意思是说 上面呢还是各种判断校验,判断是什么类型很显然是Obj类型的 

3  desc.isInstantiable() 是不是实例化, 

4 看注释大概知道 那也就是说我们现在这类我们实现了Serializable这个接口,那么在objectStreamclass类里边,这个判断就会返回true,返回之后再回来到这里边,它为true的话那就会new一个instance,然后把Obj返回去。  

这个对象是通过反射创建出来的对象new instance对象.是一个创建一个object返回回去 
  class类型的,那既然通过反射去创建对象, 那肯定和之前的对象不是同一个,这就解释了。为什么刚刚我们序列化和反序列化把单例模式破坏了 

那怎么解决呢?   在实例里面加一个方法:   

public class SingletonObject1 implements Serializable {

    /**
     * can't lazy load.
     */
    private static final SingletonObject1 instance = new SingletonObject1();

    private SingletonObject1() {
        //empty
    }

    public static SingletonObject1 getInstance() {
        return instance;
    }

    //新加一个方法 防止被反序列化改变对象 
    private Object readResolve() {
        return instance;
    }

}

这样,反序列出来就是一样的对象    那么为什么加这么一个方法就不会被改变对象呢? 

 

1因为上面被创建了,所以它肯定不是null  就到了 Object rep = desc.invokeReadResolve(obj);方法里面 

2 从invokeReadResolve 方法注释上写的 我们大概知道,如果改类实现了readResolve 方法,就通过反射 调用这个方法 

所以他们的对象是一样的,但是从实际意义上,在内存中多了一个实例,但是没被引用  

那么问题又来了我们还可以通过反射方法去创建一个新的啊 

如下面这样 :  

  //获取对象
        Class obj = Class.forName(SingletonObject1.class.getName());
        //获取声明的构造器
        Constructor declaredConstructor = obj.getDeclaredConstructor();
        //这个做法是把 SingletonObject1 里面的private权限放开了
        declaredConstructor.setAccessible(true);
        //自己获取一个
        SingletonObject1 instance1 = SingletonObject1.getInstance();
        Object instance2 = declaredConstructor.newInstance(SingletonObject1.class);
        System.out.println(instance1);
        System.out.println(instance2);
        System.out.println(instance1 ==instance2);

结果很遗憾,还是为fasle 

所以我们在 构造器里面加上判断  
 

public class SingletonObject1 implements Serializable {

    /**
     * can't lazy load.
     */
    private static final SingletonObject1 instance = new SingletonObject1();

    private SingletonObject1() {
        //empty
        if(instance != null){
            throw new RuntimeException("单例构造器禁止反射调用");
        }

    }

    public static SingletonObject1 getInstance() {
        return instance;
    }

    //新加一个方法 是反射出来的
    private Object readResolve() {
        return instance;
    }

}

更改一下  

public class SingletonObject6 {

   
    //static 只会被初始化一次, 同时也只能会顺序执行 被修饰的主动加载,只有使用它才会被加载
    //初始化,构建,是线程友好的不会被初始化两次
    private static class InstanceHolder {
        private final static SingletonObject6 instance = new SingletonObject6();
    }

    public static SingletonObject6 getInstance() {
        return InstanceHolder.instance;
    } 

    private SingletonObject6() {
        //构造私有化
         if(instance .staticInnerClassSingleton != null){
            throw new RuntimeException("单例构造器禁止反射调用");
         }
    }
}

那么问题来了我们能不能通过某个属性来条带这个判断呢? 如: 

public class LazySingleton {
    private static LazySingleton lazySingleton = null;
    private  static boolean flag =true;
    private LazySingleton(){
        if (flag){
            flag=false;
        }else {
            throw new RuntimeException("单例构造器禁止反射调用");
        }
    }
    public synchronized static LazySingleton getInstance(){
        if(lazySingleton == null){
            lazySingleton = new LazySingleton();
        }
        return lazySingleton;
    }
}

答案是不能的因为我们对flag这个值进行修改  

  public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException, NoSuchFieldException {
        Class objectClass = LazySingleton.class;
        Constructor c = objectClass.getDeclaredConstructor();
        c.setAccessible(true);

        LazySingleton o1 = LazySingleton.getInstance();
        //获取到flag 这个字段
        Field flag = o1.getClass().getDeclaredField("flag");
        //修改权限也就是说private 失效了
        flag.setAccessible(true);
        flag.set(o1,true);


        LazySingleton o2 = (LazySingleton) c.newInstance();

        System.out.println(o1);
        System.out.println(o2);
        System.out.println(o1==o2);
    }

这样我们通过反射攻击成功!!! 

总结了这么长,其实最推荐的还是枚举这种方法,是被人推荐的  

当然面试官会让你举几个单例模式的例子  很简单啊JDK 的 恶汉式的 : 

public class Runtime {
    private static Runtime currentRuntime = new Runtime();

    /**
     * Returns the runtime object associated with the current Java application.
     * Most of the methods of class <code>Runtime</code> are instance
     * methods and must be invoked with respect to the current runtime object.
     *
     * @return  the <code>Runtime</code> object associated with the current
     *          Java application.
     */
    public static Runtime getRuntime() {
        return currentRuntime;
    }

 

 

 

好了好了,一个单例写了这么长时间,如果面试的时候这样回答,保证让面试官爱上你哦!!!

 

  • 9
    点赞
  • 52
    收藏
    觉得还不错? 一键收藏
  • 5
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值