美文网首页
剑指Offer - 单例模式

剑指Offer - 单例模式

作者: 顶级工程师闯天涯 | 来源:发表于2018-05-06 23:34 被阅读30次
    Future.png

    单例模式

    We all want to as a 有追求的程序猿,这不将早已尘封的《剑指Offer》給拿出来重新拜读一下。该书中面试题2就是单例模式,可见其重要性(Maybe just for Interview).同时也为了这次更加系统地阅读&总结。
    PS :之前也是随便翻了几下就束之高阁啦。


    前言

    我们在面试中经常遇到单例模式(However you as 面试者or面试官),关于单例模式的优秀文章,网上也是俯首皆是。本文Just for me to 心得体会or笔记。如果有幸能帮助到其他人,那我将会更加高兴...

    1-饿汉式单例

    这个写法就类似于解决了单例模式中的“温饱问题”

    public class Singleton {
      // JVM加载该类时,单例对象就会自动创建
        private static Singleton instance = new Singleton();
    
        private Singleton() {
            System.out.println("构造函数Running....");
        }
    
        public static Singleton getInstance(){
            return instance;
        }
    
        /**
         * 证明了没有对instance做延时加载...
         */
        public static void doSomething(){
            System.out.println("Just for fun...");
        }
    
        public static void main(String[] args) {
            /*
            * 这里没有用到该实例,But 照样给我创建了其实例
            */
            Singleton.doSomething();
        }
    }
    
    

    "Talk is cheap,show me the code"

    JVM类的加载原理
    1. JVM在执行类的初始化期间,JVM会获得一把锁,该锁可以同步多个线程对同一个类的初始化

    There is no doubt that 该种方案实现简单,且线程安全。但是其没有对instance做相应的延时加载,只要初始化该类就创建其实例,这样就造成了资源浪费。


    2-懒汉式单例

    “懒汉式”---顾名思义,就是你要我才給,按需分配

    /**
     * “懒汉式”,用到实例才加载,否则不加载
     *
     * 缺点:线程不安全...
     */
    public class Singleton {
        private static Singleton instance = null;
    
        private Singleton() {
            System.out.println("构造函数Running...");
        }
    
        public  static Singleton getInstance(){
            /**
             * 避免重复创建...
             */
            if (instance == null) {
                instance = new Singleton();
            }
           return instance;
        }
    
        /**
         * HashCode相等说明是同一个实例
         * @return
         */
        @Override
        public int hashCode() {
            return super.hashCode();
        }
    
        public static void main(String[] args) {
            /**
             * 模拟多线程环境,会发现不是同一个实例...
             */
            for (int i = 0; i < 5; i++) {
                new Thread(() -> System.out.println(getInstance().hashCode())).start();
            }
        }
    }
    

    Code 地址:该编辑器在多线程情况下测试单例不好使,有兴趣可以复制到本地去运行测试

    单例模式.png

    (PS:原图片链接

    线程不安全,那我们只能去使用同步机制来保证线程安全

    3-同步锁的懒汉式

    /**
     *
     * 保证线程安全的“饿汉式”单例
     *
     * 即:加入synchronized 同步关键字...
     *
     * 下面的格式将会造成:每次来调用getInstance()都要进行线程同步(即调用synchronized锁)
     *
     * 而实际上, 只需要在第一次调用的时候才需要进行同步,只要单例存在,就没必要进行同步啦...
     *
     */
    public class Singleton {
        private static Singleton instance = null;
    
        private Singleton() {
            System.out.println("构造函数running...");
        }
    
    
        public static synchronized Singleton getInstance(){
            if (instance == null) {
                instance = new Singleton();
            }
            return instance;
        }
    
        /**
        * 也可以写成这种格式
        * */
    
    //    public static Singleton getInstance(){
    //        synchronized (Singleton.class){
    //            if (instance != null) {
    //                instance = new Singleton();
    //            }
    //        }
    //        return instance;
    //    }
        
        @Override
        public int hashCode() {
            return super.hashCode();
        }
        
        public static void main(String[] args) {
            for (int i = 0; i <5 ; i++) {
                new Thread(() -> System.out.println(Singleton.getInstance().hashCode())).start();
            }
        }
    }
    
    

    Source code

    代码中的注释已经很清楚了,这里就闲话不扯...

    4-双重校验锁(Double-Check)单例

    这个是重点...

    
    /**
     * "Double-Check"
     * "双重校验锁"的单例...
     *
     *
     * 其实就是在"同步锁"的基础上,外层 + if 判断...
     *
     * 作用:若单例存在,就不需要进行同步加锁操作synchronized。直接返回实例。从而提高程序性能...
     *
     *
     * PS: 到这里还没有完工,主要原因在于 instance = new Singleton2();
     * 这并非是个原子操作,该句事实上在JVM中大概有三个过程:
     * 1. 給instance 分配内存;
     * 2. 调用Singleton2的构造函数来初始化成员变量,生成实例;
     * 3. 将singleton对象指向分配的内存空间(此时,instance 才是非null的)
     *
     *
     * 但是在JVM的即时编译器中存在指令重排的优化,so 上述的2,3顺序不能保证。
     * 假如执行序列为1-3-2.,当3执行完毕,而2未执行之前,被其他线程抢占了,此时instance已经是非null(但是没有初始化)
     * 线程直接返回了instance,然后使用就报错...
     *
     * 所以需要在instance声明为volatile 就可以啦...
     *
     *
     * volatile关键字的两个功能:
     * 1. 这个变量不会在多个线程中存在副本,直接从内存中读取...
     * 2. 禁止指令重排序优化。
     *
     * 但是这个只在Java 1.5之后有效,因为之前的Java内存模型有缺陷...
     *
     * 总结:
     * 该单例版本有点复杂...
     *
     */
    public class Singleton {
        /**
         * 注意这里...volatile关键字
         */
        private volatile static Singleton instance = null;
    
        private Singleton() {
            System.out.println("running...");
        }
    
        public static Singleton getInstance() {
            if (instance == null) {
                synchronized (Singleton.class) {
                    if (instance == null) {
                        instance = new Singleton();
                    }
                }
            }
            return instance;
        }
    
        @Override
            public int hashCode() {
            return super.hashCode();
        }
    
        public static void main(String[] args) {
            for (int i = 0; i < 5; i++) {
                new Thread(() -> System.out.println(getInstance().hashCode())).start();
            }
        }
    }
    
    

    双重校验锁Source Code

    双重校验锁是满足要求,But 有局限性... 别着急,还有更好的

    5- 静态内部类实现单例

    package offer;
    
    /**
     * @author king
     * @date 2018/5/6
     * <p>
     * 静态内部类实现单例
     */
    public class Singleton {
        /**
         * 创建静态内部类
         */
        private static class InnerSingleton {
            /**
             * 在静态内部类里创建单例
             */
            private static Singleton instance = new Singleton();
        }
    
        /**
         * 私有化构造函数
         */
        private Singleton() {
            System.out.println("构造函数Running...");
        }
    
        public static Singleton getInstance() {
            return InnerSingleton.instance;
        }
    
        @Override
        public int hashCode() {
            return super.hashCode();
        }
    
        public static void main(String[] args) {
            for (int i = 0; i < 5; i++) {
                new Thread(() -> System.out.println(getInstance().hashCode())).start();
            }
        }
    }
    
    

    静态内部类单例Source Code


    关于单例的小插曲

    曾经在面试过程中,问到面试者饿汉式&懒汉式的区别:

    曾有面试者告诉我,饿汉式每次加载类都会 new 一次对象,将造成资源浪费。我当时没反应过来,只注意到它回答的“资源浪费”,后来我才明白过来,instance是static的,只会初始化一次,何来多次new 之谈???

    1. 有一次让面试者写个懒汉式单例(先不考虑多线程情况),代码大意如下:
    public class Singleton {
      public static  Singleton instance = null;
      private Singleton(){
      }
      //  注意...
      public static Singleton getInstance(){
        instance = new Singleton();
        return instance;
      }
    
    }
    

    少个判空,已非“单例”啊...

    总结

    无论是剑指offer这本书,还是我们面试中高级岗位时,考察点基本都会设在双重校验锁上,毕竟面试造核弹...

    参考文章

    单例模式
    最全面的单例讲解
    深入浅出Singleton

    相关文章

      网友评论

          本文标题:剑指Offer - 单例模式

          本文链接:https://www.haomeiwen.com/subject/ydlprftx.html