美文网首页
单例模式

单例模式

作者: vonnie | 来源:发表于2017-09-19 22:06 被阅读19次

    说到单例模式,大家应该都不陌生,毕竟它是应用最广泛的模式之一。

    单例模式的主要实现形式

    饿汉模式

    饿汉模式是在声明静态对象时就已经初始化单例了。代码如下:

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

    缺点:无论使用还是不使用都会初始化,造成不必要的开销。

    懒汉模式

    懒汉模式实在第一次调用getInstance的时候去初始化。代码如下:

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

    优点:只有在第一次使用的时候才会去实例化单例。

    缺点:每次调运都会去同步,造成不必要的同步开销。

    double check lock(DCL)实现单例

    DCL实现单例的优点是既能在需要的时候才初始化单例,又能保证线程安全,而且单例初始化后,调用getInstance没有同步开销。代码如下:

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

    但是,这种模式存在缺陷,就是在高并发的情况下会出问题。这是为什么了?下面来分析一下。

    mInstance = new Singleton();它不是一个原子操作,这个行代码最终会被编译成汇编指令,它大致做了三件事:

    1. 给Singleton实例分配内存空间
    2. 调运Singleton()初始化
    3. 将mInstance指向分配好的内存空间(这时mInstance就不为null了)

    但是jvm为了优化指令,提高运算效率就会进行指令重排,导致2、3不一定是顺序执行的。也就是说执行的顺序可能是1-2-3,也可能是1-3-2。这就尴尬了,当A线程执行了1-3,此时mInstance已经不为null了,但是它指向的内存是不可用的,此时B线程调运了getInstance(),返现mInstance不为null,所以就直接使用了,这时就会出错。这就是DCL失效的问题,而且这种难以追踪难以重现的问题会隐藏很久。

    指令重排是什么?下面来介绍一下

    指令重排

    指令重排序是JVM为了优化指令,提高程序运行效率,在不影响单线程程序执行结果的前提下,尽可能地提高并行度。编译器、处理器也遵循这样一个目标。

    不同的指令间可能存在数据依赖。比如下面计算圆的面积的语句:

    double r = 2.3d;//(1)
    double pi =3.1415926; //(2)
    double area = pi* r * r; //(3)
    

    area的计算依赖于r与pi两个变量的赋值指令。而r与pi无依赖关系。

    as-if-serial语义是指:不管如何重排序(编译器与处理器为了提高并行度),(单线程)程序的结果不能被改变。这是编译器、Runtime、处理器必须遵守的语义。

    虽然,(1) – happens before -> (2),(2) – happens before -> (3),但是计算顺序(1)(2)(3)与(2)(1)(3) 对于r、pi、area变量的结果并无区别。编译器、Runtime在优化时可以根据情况重排序(1)与(2),而丝毫不影响程序的结果。

    指令重排序包括编译器重排序和运行时重排序。

    防止指令重排

    JDK1.5之后sun公司注意到了这个问题,就增加了volatile。可以使用volatile变量禁止指令重排序。变量在以volatile修饰后,会阻止JVM对与其相关的代码进行重排,达到按照既定顺序执行代码的目的。代码如下:

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

    静态内部类单例模式

    DCL虽然解决了资源消耗、多余同步、线程安全等问题,但是在某些情况下,它还会出现实效的情况。在《java并发编程》一书中指出DCL是一种丑陋的写法,不赞成使用。并建议使用如下写法:

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

    但加载Singleton类的时候,并不会去初始化mInstance,只有第一次调用getInstance的时候在回去初始化mInstacnce。第一次调用getInstance的时候,会导致虚拟机去加载SingletonHolder类,这时才会初始化mInstance。

    相关文章

      网友评论

          本文标题:单例模式

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