zoukankan      html  css  js  c++  java
  • 单例模式

    单例模式

    单例模式, 顾名思义就是只有一个实例, 并且它自己负责创建自己的对象, 这个类提供了一种访问其唯一的对象的形式. 可以直接访问, 不需要实例化该类的对象.


    单例模式的几种形式

    饿汉式

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

    实例在类初始化的时候就创建好了, 不管你有没有用到. 好处是没有线程安全问题, 坏处是比较浪费内存空间.


    懒汉式

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

    懒汉式, 顾名思义就是实例在用到的时候才去创建.
    有线程安全和不安全两种写法, 区别就是synchronized关键字


    双检锁

    class Singleton {
        private Singleton() { }
        private static Singleton instance;
        public static Singleton newInstance() {
            if (instance == null) {		// 第一次检查, 有则返回  (此时尚未加锁, 可以提高效率)
    			   synchronized(Singleton.class) {
    					if (instance == null) {		// 第二次检查, 再获取到锁之间, 再次检查是为完成初始化
    						instance = new Singleton();
    					}
    			   }
            }
            return instance;
        }
    }
    

    双检锁, 又叫双重校验锁, 综合了饿汉式和懒汉式两者的优缺点整合而成, 从上面的代码实现看, 特点是在 synchronized关键字内外都加了一层 if条件判断, 这样既保证了线程安全, 又比直接上锁提高了执行效率, 还节省了内存空间. (其实有坑)


    静态内部类

    class Singleton {
        private Singleton() { }
        private static class Inner {
    		  private static Singleton instance = new Singleton();
    	  }
        public static Singleton newInstance() {
            return instance;
        }
    }
    
    • 静态内部类不会随着外部类的初始化而初始化, 它是需要单独去加载和初始化的, 当第一次执行 getInstance()方法时, Inner类会被初始化.
    • 静态对象 instance的初始化在 Inner类初始化阶段进行, 类初始化阶段即虚拟机执行类构造器<client>() 方法的过程.
    • 虚拟机会保证一个类的 <client>()方法在多线程环境下被正确地加锁和同步, 如果多个线程同时初始化一个类, 只会有一个线程执行这个类的 <client>方法, 其他线程都会阻塞等待.

    枚举

    enum Singleton {
        INSTANCE;
        public void doSomething() {
            System.out.println("do something ...");
        }
    }
    

    枚举的方式是比较少见一种的实现方式. 但是看上面的代码, 却简洁清晰. 并且它还自动支持序列化机制, 绝对防止多次实例化.


    传统单例模式双重检查锁存在的问题

    单例模式 1.0

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

    这种方式很辣鸡, 因为多线程环境下不能保证单例.


    单例模式 2.0

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

    这种方式也很辣鸡, 因为多线程环境下, 每个线程执行 getInstance()都要阻塞, 效率很低.


    单例模式 3.0

    class Singleton {
        private Singleton() { }
        private static Singleton instance;
        public static Singleton newInstance() {
            if (instance == null) {		// 位置1
    			   synchronized(Singleton.class) {
    					if (instance == null) {
    						instance = new Singleton();		// 位置2
    					}
    			   }
            }
            return instance;
        }
    }
    

    这种方式使用双重检查锁, 多线程环境下执行 getInstance()时, 先判断单例对象是否已初始化, 如果已经初始化, 就直接返回单例对象, 如果未初始化, 就在同步代码快中先完成初始化, 然后返回, 效率很高.

    但是: 这种方式是一个错误的优化, 问题的根源出现在位置2,
    instance = new Singleton(); 这句话创建了一个对象, 它可以分解成如下3行代码:

    memory = allocate();    // 1. 分配对象的内存空间
    ctorInstance(memory);   // 2. 初始化对象
    instance = memory;      // 3. 设置 instance指向刚分配的内存地址.
    

    上述伪代码中的 2和3之间可能发生重排序, 重排序后的执行顺序如下.

    memory = allocate();   // 1.分配对象的内存空间
    instance = memory;     // 2.设置instance指向刚分配的内存地址,此时对象还没有被初始化
    ctorInstance(memory);   // 3.初始化对象
    

    因为这种重排序并不影响 java规范中的规范, intra-thread sematics允许那些在单线程内不会改变单线程程序执行结果的重排序.
    但是多线程并发时可能会出现以下情况: 线程B访问到的是一个还未初始化的对象

    双检锁单例模式的问题演示图

    解决方案1:

    将对象声明为 volatile后, 前面的重排序在多线程环境下将被禁止.
    
    class Singleton {
        private Singleton() { }
        private static volatile Singleton instance;
        public static Singleton newInstance() {
            if (instance == null) {		// 位置1
    			   synchronized(Singleton.class) {
    					if (instance == null) {
    						instance = new Singleton();		// 位置2
    					}
    			   }
            }
            return instance;
        }
    }
    

    解决方案2:

    class Singleton {
        private Singleton() { }
        private static class Inner {
    		  private static Singleton instance = new Singleton();
    	  }
        public static Singleton newInstance() {
            return instance;
        }
    }
    
    • 静态内部类不会随着外部类的初始化而初始化, 它是需要单独去加载和初始化的, 当第一次执行 getInstance()方法时, Inner类会被初始化.
    • 静态对象 instance的初始化在 Inner类初始化阶段进行, 类初始化阶段即虚拟机执行类构造器<client>() 方法的过程.
    • 虚拟机会保证一个类的 <client>()方法在多线程环境下被正确地加锁和同步, 如果多个线程同时初始化一个类, 只会有一个线程执行这个类的 <client>方法, 其他线程都会阻塞等待.

    似乎静态内部类看起来已经是最完美的方法了, 其实不是, 可能还存在反射攻击或者反序列化攻击. 且看如下代码:

    public static void main(String[] args) throws Exception {
        Singleton singleton = Singleton.getInstance();
        Constructor<Singleton> constructor = Singleton.class.getDeclaredConstructor();
        constructor.setAccessible(true);
        Singleton newSingleton = constructor.newInstance();
        System.out.println(singleton == newSingleton);		// 运行结果:  false
    }
    

    通过结果看, 这两个实例不是同一个, 这就违背了单例模式的原则了. 出了反射攻击之外, 还可能存在序列化攻击的情况.

    class Singleton8 implements Serializable {
        private Singleton8() {}
        private static class SingletonHolder {
            private static Singleton8 instance = new Singleton8();
        }
        public static Singleton8 getInstance() {
            return SingletonHolder.instance;
        }
    
        public static void main(String[] args) {
            Singleton8 instance = Singleton8.getInstance();
            byte[] serialize = SerializationUtils.serialize(instance);
            Singleton8 newInstance = SerializationUtils.deserialize(serialize);
            System.out.println(instance == newInstance);        // false
        }
    }
    

    解决方案3:

    通过枚举实现单例模式
    <<Effective Java>>书中说道, 最佳的单例实现模式就是枚举模式. 利用枚举的特性, 让 JVM来帮我们保证线程安全和单一实例. 初次之外, 写法还特别简单.

    enum Singleton {
        INSTANCE;
        public void doSomething() {
            System.out.println("do something ...");
        }
    }
    

    参考文章

    1. 单例模式的五种写法_absolute_chen的博客-CSDN博客
    2. 传统单例模式双重检查锁存在的问题 - 君奉天 - 博客园
    3. Java单例模式:为什么我强烈推荐你用枚举来实现单例模式 - happyjava - 博客园
  • 相关阅读:
    [Erlang 0106] Erlang实现Apple Push Notifications消息推送
    一场推理的盛宴
    [Erlang 0105] Erlang Resources 小站 2013年1月~6月资讯合集
    [Erlang 0104] 当Erlang遇到Solr
    [Erlang 0103] Erlang Resources 资讯小站
    history.go(-1)和History.back()的区别
    [Java代码] Java用pinyin4j根据汉语获取各种格式和需求的拼音
    spring中context:property-placeholder/元素
    Java中的异常处理:何时抛出异常,何时捕获异常?
    用Jersey构建RESTful服务1--HelloWorld
  • 原文地址:https://www.cnblogs.com/bobo132/p/13950336.html
Copyright © 2011-2022 走看看