zoukankan      html  css  js  c++  java
  • 实现优雅的单例模式

    1、想到单例模式,根据经验写的代码如下:

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

    懒汉模式: 如果单利初始化值为null

    饿汉模式:如果单例对象一开始就被new Siglton() 主动构建,不需要判空操作

    以上写法有三点理由:

      a)  要想让一个类只能构建一个对象,自然不能让它随便去做new操作,因此Signleton的构造方法是私有的。

      b)  instance是Singleton类的静态成员,也是我们的单例对象。它的初始值可以写成Null,也可以写成new Singleton()。至于其中的区别后来会做解释。

      c)  getInstance是获取单例对象的方法。

      如果单例初始值是null,还未构建,则构建单例对象并返回。这个写法属于单例模式当中的懒汉模式。

      如果单例对象一开始就被new Singleton()主动构建,则不再需要判空操作,这种写法属于饿汉模式

    2、上述代码已经实现了一个单例模式,但是并不完美

      非线程安全的单例模式,当两个线程同时去访问 getInstance时, 可能都认为 instance为空,继而都创建一个 Siglton 对象。

      这样一来,Siglton被创建了两次。接下来实现线程安全的单例模式:

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

    理由:

      a)   为了防止new Singleton被执行多次,因此在new操作之前加上Synchronized 同步锁,锁住整个类(注意,这里不能使用对象锁)。

      b)   进入Synchronized 临界区以后,还要再做一次判空。因为当两个线程同时访问的时候,线程A构建完对象,线程B也已经通过了最初的判空验证,不做第二次判空的话,线程B还是会再次构建instance对象。

    3、JVM编译器的指令重排。

              表面上看上述分析似乎没有任何问题,要么Instance还没被线程A构建,线程B执行 if(instance == null)的时候得到false;要么Instance已经被线程A构建完成,线程B执行 if(instance == null)的时候得到true。

            指令重排是什么意思呢?比如java中简单的一句 instance = new Singleton,会被编译器编译成如下JVM指令:

                  memory =allocate();      //1:分配对象的内存空间 

                  ctorInstance(memory);  //2:初始化对象 

                  instance =memory;       //3:设置instance指向刚分配的内存地址

    但是这些指令顺序并非一成不变,有可能会经过JVM和CPU的优化,指令重排成下面的顺序:

                 memory =allocate();      //1:分配对象的内存空间 

                 instance =memory;        //3:设置instance指向刚分配的内存地址 

                 ctorInstance(memory);  //2:初始化对象 

    当线程A执行完1,3,时,instance对象还未完成初始化,但已经不再指向null。此时如果线程B抢占到CPU资源,执行  if(instance == null)的结果会是false,从而返回一个没有初始化完成的instance对象。如下图所示:

    第三版的代码如下:(instanc 变量增加了 volatile 限定)   

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

    }
    return instance;
    }
    }

    volatile 限定符 阻止了变量访问前后的指令重排,保证了指令的访问顺序。

      经过volatile的修饰,当线程A执行instance = new Singleton的时候,JVM执行顺序是什么样?始终保证是下面的顺序:

                memory =allocate();       //1:分配对象的内存空间 

                ctorInstance(memory);  //2:初始化对象 

                instance =memory;       //3:设置instance指向刚分配的内存地址 

    如此在线程B看来,instance对象的引用要么指向null,要么指向一个初始化完毕的Instance,而不会出现某个中间态,保证了安全。

    总结: 

      a) 使用双重锁检测机制    (DCL :double checked locking),确保并发情况下instance 对象不会被重复初始化。

      b) 使用 volatile 修饰符,防止指令重排引发的初始化问题。

      c) 通过 反射 的方法访问该类时仍然可以构建多个实例对象。

    4、用静态类实现 单例模式

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

    这里有几个需要注意的点:

      a) 从外部无法访问静态内部类LazyHolder,只有当调用Singleton.getInstance方法的时候,才能得到单例对象INSTANCE。

      b)INSTANCE对象初始化的时机并不是在单例类Singleton被加载的时候,而是在调用getInstance方法,使得静态内部类LazyHolder被加载的时候。因此这种实现方式是利用classloader的加载机制来实现懒加载,并保证构建单例的线程安全。

      c)  饿汉模式:因为单例是静态的final变量,当类第一次加载到内存中的时候就初始化了,所以创建的实例固然是thread-safe。

      d)  和3中一样并不能保证反射安全。可以进行如下验证: 

    //获得构造器
    Constructor con = Singleton.class.getDeclaredConstructor();
    //设置为可访问
    con.setAccessible(true);
    //构造两个不同的对象
    Singleton singleton1 = (Singleton)con.newInstance();
    Singleton singleton2 = (Singleton)con.newInstance();
    //验证是否是不同对象
    System.out.println(singleton1.equals(singleton2));

    5、通过枚举类型来实现 单例模式

    public enum SingletonEnum {
        INSTANCE;
    }

    可以通过EasySingleton.INSTANCE来访问,这比调用getInstance()方法简单多了。

    通过枚举类型构造的单例模式会 阻止反射获取枚举的私有构造方法。同样执行上述 反射判断的代码会抛出如下异常:

    Exception in thread "main" java.lang.NoSuchMethodException: com.heitian.ssm.utils.SingletonEnum.<init>()
    at java.lang.Class.getConstructor0(Class.java:2892)
    at java.lang.Class.getDeclaredConstructor(Class.java:2058)
    at com.heitian.ssm.utils.SingletonTest.main(SingletonTest.java:22)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:606)
    at com.intellij.rt.execution.application.AppMain.main(AppMain.java:134)

      枚举单例有序列化和线程安全的保证,同时代码简单。下面看一个具体的通过 枚举实现单例模式的例子:

    class Resource{
    }
    
    public enum SomeThing {
        INSTANCE;
        private Resource instance;
        SomeThing() {
            instance = new Resource();
        }
        public Resource getInstance() {
            return instance;
        }
    }

    上面的类Resource是我们要应用单例模式的资源,具体可以表现为网络连接,数据库连接,线程池等等。 
    获取资源的方式很简单,只要 SomeThing.INSTANCE.getInstance() 即可获得所要实例。

    为什么枚举能保证  线程安全/单例:

      首先,在枚举中我们明确了构造方法限制为私有,在我们访问枚举实例时(INSTANCE)会执行构造方法,同时每个枚举实例都是static final类型的,

    也就表明只能被实例化一次。在调用构造方法时,我们的单例被实例化。 
      也就是说,因为enum中的实例被保证只会被实例化一次,所以我们的INSTANCE也被保证实例化一次。 

    总结

    序列化问题:

      使用枚举实现的单例模式,不但可以防止利用反射强行构建单例对象,而且可以在枚举类对象被反序列化的时候,保证反序列的返回结果是同一对象。

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

      传统单例存在的另外一个问题是一旦你实现了序列化接口,那么它们不再保持单例了,因为readObject()方法一直返回一个新的对象就像java的构造方法一样,

    你可以通过使用readResolve()方法来避免此事发生,看下面的例子:

    //readResolve to prevent another instance of Singleton
        private Object readResolve(){
            return INSTANCE;
        }

    这样甚至还可以更复杂,如果你的单例类维持了其他对象的状态的话,因此你需要使他们成为transient的对象(?)。但是枚举单例,JVM对序列化有保证。

     使用总结:

            单例模式虽然有很多优点,但是并不是所有的场景都适合使用单例模式。 当一个程序需要对应不同的实例,或者有不同参数需求的情况下,单例

    模式显然不能满足需求。 如果仅仅一味的强制使用单例模式,可能会带来不可预知的问题。

           实际开发中,先使用了单例模式(构造参数中含有UserId)。但是后来业务提出需求,当用户切换账号后,要还能继续调用接口。此时如果继续使用单例模式,

    UserId 由A变成B后,A继续调用该单例模式就会出现问题。

    参考:

    http://www.mamicode.com/info-detail-1728587.html

  • 相关阅读:
    数学+高精度 ZOJ 2313 Chinese Girls' Amusement
    最短路(Bellman_Ford) POJ 1860 Currency Exchange
    贪心 Gym 100502E Opening Ceremony
    概率 Gym 100502D Dice Game
    判断 Gym 100502K Train Passengers
    BFS POJ 3278 Catch That Cow
    DFS POJ 2362 Square
    DFS ZOJ 1002/HDOJ 1045 Fire Net
    组合数学(全排列)+DFS CSU 1563 Lexicography
    stack UVA 442 Matrix Chain Multiplication
  • 原文地址:https://www.cnblogs.com/NeilZhang/p/7979629.html
Copyright © 2011-2022 走看看