美文网首页
设计模式之单例模式

设计模式之单例模式

作者: Mr靖哥哥 | 来源:发表于2020-07-23 23:31 被阅读0次

    一、概念

    JVM中,单例对象只有一个实例存在。

    二、饿汉式实现

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

    最简单的实现方式,但是如果对象的构造耗费时间,可能采用懒汉式更好。

    三、懒汉式实现一

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

    也是很简单粗暴的懒汉式实现方式,每次获取单例的时候都需要获取排他锁,效率差。

    四、懒汉式实现二

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

    1、双重判断

    • 第一次判断是防止实例化完毕后的无效同步处理
    • 如果没有第二次校验,假设线程t1执行了第一次校验后,判断为null,这时t2也获取了CPU执行权,也执行了第一次校验,判断也为null。接下来t2获得锁,创建实例。这时t1又获得CPU执行权,由于之前已经进行了第一次校验,结果为null(不会再次判断),获得锁后,直接创建实例。结果就会导致创建多个实例。所以需要在同步代码里面进行第二次校验,如果实例为空,则进行创建。

    2、指令重排

    指令重排序是JVM为了优化指令,提高程序运行效率,在不影响单线程程序执行结果的前提下,尽可能地提高并行度。
    也就是说,JVM为了执行效率会将指令进行重新排序,但是这种重新排序不会对单线程程序产生影响。

    由于instance = new Singleton();操作并不是一个原子性指令,会被分为多个指令:

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

    但是经过重排序后如下:

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

    3、可见性

    instance需要加volatile关键字,否则会出现错误。问题的原因在于JVM指令重排优化的存在。在某个线程创建单例对象时,在构造方法被调用之前,就为该对象分配了内存空间(空白内存)并将分配的内存地址赋值给instance字段了,然而该对象可能还没有初始化。若紧接着另外一个线程来调用getInstance,取到的就是状态不正确的对象,程序就会出错。

    五、懒汉式实现三

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

    1、加载时机

    内部静态类是要在有引用了以后才会装载到内存的,所以在你第一次调用getInstance()之前,SingletonHolder是没有被装载进来的,只有在你第一次调用了getInstance()之后,里面涉及到了return SingletonHolder.instance; 产生了对SingletonHolder的引用,内部静态类的实例才会真正装载。这也就是懒加载的意思。

    遇到new、getstatic、putstatic、或invokestatic这4条字节码指令时,如果类没有进行过初始化,则需要先触发其初始化,生成这4条指令的最常见的Java代码场景是:使用new关键字实例化对象的时候,读取或设置一个类的静态字段(被final修饰、已在编译期把结果放入常量池的静态字段除外)的时候,以及调用一个类的静态方法的时候。

    2、线程安全

    虚拟机会保证一个类的<clinit>()方法在多线程环境中被正确的加锁、同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的<clinit>()方法,其他线程都需要阻塞等待,只到活动线程执行<clinit>()方法完毕。

    相关文章

      网友评论

          本文标题:设计模式之单例模式

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