美文网首页
扩展篇之单例以及双重检测的缺陷

扩展篇之单例以及双重检测的缺陷

作者: 知止9528 | 来源:发表于2019-01-18 07:03 被阅读20次

    定义:整个系统只允许存在一个实例
    首先 我们一般都这样

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

    但这样在多线程时会存在问题
    多个线程同时运行到if (instance == null),都判断为null,那么两个线程就各自会创建一个实例。
    所以改进之后如下

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

    我们加了把锁,当然也可以加在方法上,但避免锁竞争太过激烈,一般都会较小锁的粒度,所以我们使用了代码块。这也是传说中的双重检查。

    但双重检查依然有个缺陷
    为什么呢?这要说到我们java对象的创建过程了,分三步。
    1.分配内存空间
    2.初始化对象
    3.将内存空间的地址赋值给对应的引用

    但由于重排序(可以简单理解为只保证结果一致,但过程顺序不保证,主要为了优化运行效率)的存在,2和3会进行重排序。
    所以会变成如下
    1.分配内存空间
    2.将内存空间的地址赋值给对应的引用
    3.初始化对象
    这个时候代码里的第二个判空就会出现问题,instance!=null,但实际上这个对象还是没有被初始化的。如图


    image.png

    所以这里有两种解决方案
    ①既然是重排序引起的,那我们就禁止重排序,修改如下

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

    private static volatile Singleton instance;
    我们使用了volatile 关键字来禁止了重排序

    ②运行线程A内,2和3的重排序,但不能让其它线程看到这个重排序。即B也就不会直接访问初始化对象了。
    怎么做呢?我们先看下ClassLoader下面的

    protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
        synchronized (getClassLoadingLock(name)) {
            // First, check if the class has already been loaded
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                long t0 = System.nanoTime();
                try {
                    if (parent != null) {
                        c = parent.loadClass(name, false);
                    } else {
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                    // ClassNotFoundException thrown if class not found
                    // from the non-null parent class loader
                }
    
                if (c == null) {
                    // If still not found, then invoke findClass in order
                    // to find the class.
                    long t1 = System.nanoTime();
                    c = findClass(name);
    
                    // this is the defining class loader; record the stats
                    sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                    sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                    sun.misc.PerfCounter.getFindClasses().increment();
                }
            }
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }
    }
    

    synchronized (getClassLoadingLock(name))

    即JVM在类初始化阶段就会获取一个锁,可以保证初始化instance时只有一个线程.
    所以修改如下

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

    当然这种方式,会在JVM进行类加载的时候就会进行初始化,我们想要的是只有调用getInstance方法的时候才会进行初始化.

    怎么改呢?再加一个静态内部类呗

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

    SingletonHolder是一个内部类,只在外部类的Singleton的getInstance()中被使用,所以它被加载的时机也就是在getInstance()方法第一次被调用的时候。

    当然,这些都会被反射破坏.还有的单例是用枚举来做的。

    public enum SingleInstance {
    
        INSTANCE;
    
        public void fun1() { 
            // do something
        }
    }
    SingleInstance.INSTANCE.fun1();
    

    相关文章

      网友评论

          本文标题:扩展篇之单例以及双重检测的缺陷

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