zoukankan      html  css  js  c++  java
  • Java 面试之单例模式

    以下文章来源于微信公众号小哈学Java ,作者AllenJiang

    单例模式是 23 种 GOF(设计模式) 中最简单的一种设计模式,也是最经典的一种设计模式。在 Java 面试中,可以说是必问的一个知识点了。接下来我们就来具体说一说。

    面试官:对设计模式熟悉吗?你工作中用过哪些设计模式呢?

    应聘者:工作常用的有单例模式,工厂模式,责任链模式,代理模式 …

    面试官:哦!既然你说到了单例模式,那你说说看什么是单例模式?为什么要用单例模式? 有几种单例模式?使用的时候要注意什么?

    面试官一般情况下都是通过上面一段对话,切入到单例模式这个知识点的。

    什么是单例模式?

    简单来说,单列模式是为了保证某个对象在程序的生命周期内,在内存中只存在一个实例。即一个类只有一个对象。

    为什么要用单例模式?

    使用单例模式无非是为了提高代码的执行性能,我们可以从下面两点谈起:

    • 1.节省内存资源

    • 2.节省时间(分配对象的时间)

    首先是节省内存资源,对于程序中一些大对象,这些对象占用内存较大,频繁创建不但内存开销增大,还会耗费很多内存分配的时间,另外,会触发频繁的 GC(垃圾回收),GC 又会增加时间开销,情况严重的,可能还会发生 Full GC, 导致程序主线程阻塞,这些都是不可容忍的。单列模式正是在这种情况下孕育而生的。

    有几种单例模式?

    通常情况下我们说 5 种就可以了,但是为了显示我们知识面宽泛,我们可以这样说:

    应聘者:单例模式细分的话有 7 种,但是严格来讲只有五种。先说说细分的的七种:

    第一种:懒汉,线程不安全

    public class Singleton {  
    
      private static Singleton instance;  
    
      private Singleton (){}   
    
      public static Singleton getInstance() {  
      if (instance == null) {  
        instance = new Singleton();}  
      return instance;  
      }  
    } 

    这种写法是懒加载的写法,比较致命的是,在多线程情况下,会出现多次实例化 Singleton 对象。

    第二种:懒汉,线程安全的

    public class Singleton {
      private static Singleton instance;
      private Singleton (){}
    
      public static synchronized Singleton getInstance() {
        if (instance == null) {
          instance = new Singleton();}
        return instance;
      }
    }

    这种写法能够很好的运行在多线程的环境中,也具备懒汉加载的优势,但是,很遗憾的是,效率很低,99% 的情况下不需要同步。

    第三种:饿汉式

    public class Singleton {  
    
       private static Singleton instance = new Singleton();  
    
       private Singleton (){}
    
       public static Singleton getInstance() {  
        return instance;  
    }

    这种方式是基于 java 类加载机制避免了多线程同步的问题,不过正因为如此,实例在装载时就实例化,而且导致实例化的原因还有很多种,有时候,我们是不愿意将其实例化的。而且单例模式大多数实例化都是调用 getInstance() 方法,违背了懒加载的设计。

    注意1:这里面试官可能还会问你:instance 什么时候被初始化?

    instance 在类被加载的时候就会被初始化,说到类的加载,在 JVM 虚拟机规范中并没有强制性约束在什么具体时间加载类,但是关于类的初始化,虚拟机严格规定了有且只有 4 种情况,分别是遇到下面 4 条字节码指定时:

    • 1. new

    • 2. getStatic

    • 3. putStatic

    • 4. invokeStatic

    遇到上面 4 种字节指定码时,若类没有进行过初始化,则需要先触发其初始化。

    至于什么时候生成上述 4 条指令,分别对应下面场景:

    • 1.使用 new 关键字实例化对象 ==> 对应 new 指令

    • 2.读取一个类的静态字段(被final修饰、已在编译期把结果放在常量池的静态字段除外)==> 对应 getStatic 指令

    • 3.设置一个类的静态字段(被final修饰、已在编译期把结果放在常量池的静态字段除外)==> 对应 putStatic 指令

    • 4.调用一个类的静态方法 ==> 对应 invokeStatic 指令

    注意2:面试官可能还会问到你 JVM 类的加载机制?

    这个时候你可以聊聊 classloader 双亲委派模型,这里不多赘述。

    第四种: 饿汉,变种

    public class Singleton {  
        private Singleton instance = null;  
    
        static {  
          instance = new Singleton();
        }  
    
        private Singleton (){}
    
        public static Singleton getInstance() {  
        return this.instance;  
        }  
    } 

    这里利用了 static 块来实例化对象,实际上和第三种差不多。

    第五种:静态内部类

    public class Singleton {  
    
        private static class SingletonHolder { 
          private static final Singleton INSTANCE = new Singleton();
        }  
    
        private Singleton (){}
    
        public static final Singleton getInstance() {  
          return SingletonHolder.INSTANCE;  
        }  
    } 

    这种方式同样利用了 classloder 的机制来保证初始化 instance 时只有一个线程,它跟第三种和第四种方式不同的是(很细微的差别):第三种和第四种方式是只要 Singleton 类被装载了,那么 instance 就会被实例化(没有达到 lazy loading 效果),而这种方式是 Singleton 类被装载了,instance 不一定被初始化。因为 SingletonHolder 类没有被主动使用,只有显示通过调用 getInstance 方法时,才会显示装载 SingletonHolder 类,从而实例化 instance。想象一下,如果实例化 instance 很消耗资源,我想让他延迟加载,另外一方面,我不希望在 Singleton 类加载时就实例化,因为我不能确保 Singleton 类还可能在其他的地方被主动使用从而被加载,那么这个时候实例化 instance 显然是不合适的。这个时候,这种方式相比第三和第四种方式就显得很合理。

    第六种:枚举

     public enum Singleton {  
       INSTANCE;  
       public void whateverMethod(){}  
     } 

    这种方式是 《Effective Java》 作者 Josh Bloch 提倡的方式,它不仅能避免多线程同步问题,而且还能自动避免序列化 / 反序列化攻击,以及反射攻击 (枚举类不能通过反射生成)。

    第七种: 双重校验锁(DCL)

    public class Singleton {  
    
        private volatile static Singleton singleton;  
    
        private Singleton (){}   
    
        public static Singleton getSingleton() {  
        if (singleton == null) {  
          synchronized (Singleton.class) {  
          if (singleton == null) {  
            singleton = new Singleton();}  
         }  
       }  
       return singleton;  
       }  
     } 

    这个是第二种方式的升级版,俗称双重检查锁定。通过在 synchronized 的外面增加一层判断,就可以在对象一经创建以后,不再进入 synchronized 同步块。这种方案不仅减小了锁的粒度,保证了线程安全,性能方面也得到了大幅提升。

    同时你还需要说说 volidate 这个关键词的作用是为了防止 JVM 指令重排.

    总结

    不过一般来说,第一种不算单例,第四种和第三种就是一种,如果算的话,第五种也可以分开写了。所以说,一般单例都是五种写法。

    • 1.懒汉

    • 2.饿汉

    • 3.双重校验锁(DCL)

    • 4.枚举

    • 5.静态内部类

    •  

    冷眉横对千夫指,俯首甘为孺子牛。
  • 相关阅读:
    扶桑号战列舰【单调栈+线段树】
    c++ vector基本函数、排序、查找用法
    路【邻接矩阵存图找最短路】
    free【分层图最短路】
    Magic Line【坐标点排序方法】
    Stones【中石油个人赛第十七场I】
    Second Large Rectangle【单调栈】
    点坐标求三角形面积【向量差乘】
    Random Point in Triangle【随机数解决期望值问题】
    JavaScript——基础知识,开始我们的js编程之旅吧!
  • 原文地址:https://www.cnblogs.com/yujian0817/p/12745698.html
Copyright © 2011-2022 走看看