美文网首页
单例模式

单例模式

作者: 34sir | 来源:发表于2018-02-24 15:29 被阅读7次

    延迟加载

    延迟加载是指等到真正使用时去创建实例,不使用时不创建实例

    对比延迟加载(懒汉式)和非延迟加载(饿汉式):

    • 从速度和反应时间来看,饿汉式好
    • 从资源利用效率来看,懒汉式好

    设计方式

    非延迟加载

    • 非延迟加载(饿汉)
    public class SingleTon {
        private static SingleTon instance = new SingleTon();  //注意是private
    
        private SingleTon() {//私有构造方法  防止直接new对象
        }
    
        public static SingleTon getInstance() {
            return instance;
        }
    }
    

    延迟加载

    忽略常规的延迟加载,因为存在线程问题,一般开发过程中不会用到

    • 同步延迟加载
    public class SingleTon {
       private static SingleTon instance = null;
    
        private SingleTon() {//私有构造方法  防止直接new对象
        }
    
        public static synchronized SingleTon getInstance() {
            if(instance==null){
                instance=new SingleTon(); 
            }
            return instance;
        }
    }
    
    • 双重检测同步延迟加载
    public class SingleTon {
       private static SingleTon instance = null;
    
        private SingleTon() {//私有构造方法  防止直接new对象
        }
    
        public  static  SingleTon getInstance() {
            if (instance==null){
                synchronized (SingleTon.class){
                    if(instance==null){ //此处再做一次判断是防止线程1验证过此条件new SingleTon(),此时线程2也验证过此条件,就会再次new SingleTon()
                        instance=new SingleTon();
                    }  
                }
            }
            return instance;
        }
    }
    

    对比同步延迟加载,对instance进行二次检查,目的是为了避免过多的同步

    然而上述方式还是存在一个问题:
    大家应该了解过JVM的指令重排,没了解过的可见:有关并发编程
    Java中类似于instance = new Singleton会被编译期编译成JVM指令:

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

    经过JVM和CPU的优化,有可能指令重排成下面的顺序:

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

    那么问题就来了:
    假设线程1已经执行了1,3,此时instance已经不为null,线程2正好执行到if(instance==null)的判断,那么线程2就会直接返回一个没有经过初始化的instance,这样显然是有问题的
    怎么解决这个问题?
    volatile 有关volatile

    public class SingleTon {
       private volatile static  SingleTon instance = null;  //volatile 阻止JVM对instance的相关操作进行指令重排
    
        private SingleTon() {//私有构造方法  防止直接new对象
        }
    
        public  static  SingleTon getInstance() {
            if (instance==null){
                synchronized (SingleTon.class){
                    if(instance==null){ //此处再做一次判断是防止线程1验证过此条件new SingleTon(),此时线程2也验证过此条件,就会再次new SingleTon()
                        instance=new SingleTon();
                    }  
                }
            }
            return instance;
        }
    }
    
    • 静态内部类实现延迟加载
      此种方法真正意义上实现了延迟加载
    public class Singleton {
        private static class LazyHolder {
            private static final Singleton INSTANCE = new Singleton();  //这里private没有什么实际意义
        }
        private Singleton (){}
        public static Singleton getInstance() {
            return LazyHolder.INSTANCE;
        }
    }
    

    注意点
    1.从外部无法访问静态内部类LazyHolder,只有当调用Singleton.getInstance方法的时候,才能得到单例对象INSTANCE
    2.INSTANCE对象初始化的时机并不是在单例类Singleton被加载的时候,而是在调用getInstance方法,使得静态内部类LazyHolder被加载的时候。因此这种实现方式是利用classloader的加载机制来实现懒加载,并保证构建单例的线程安全

    然而到这里,以上的所有的方式都不是最完美的,因为Java中反射(简直就是isis)的存在

    • 枚举
      这种方式应该属于非延迟性加载
    public enum Singleton {  
        INSTANCE;  
        public void whateverMethod() {  
        }   
    }  
    

    JVM禁止获取枚举的私有构造方法,这种方法可以完美解决反射带来的风险,不过个人感觉开发中并不需要刻意使用这种方式
    补充
    使用枚举实现的单例模式,不但可以防止利用反射强行构建单例对象,而且可以在枚举类对象被反序列化的时候,保证反序列的返回结果是同一对象

    对于其他方式实现的单例模式,如果既想要做到可序列化,又想要反序列化为同一对象,则必须实现readResolve方法

    小结

    实现方式 线程安全 懒加载 禁止反射构建
    双重锁检测
    静态内部类
    枚举

    相关文章

      网友评论

          本文标题:单例模式

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