• 实现优雅的单例模式


    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

  • 相关阅读:
    基于spark-streaming实时推荐系统
    xgb
    FM算法解析及Python实现
    FM算法
    计算广告
    转发推荐系统文章
    【spark】dataframe常见操作
    VS Code WSL 2 配置 Spring Boot 2
    Makefile
    Paper English
  • 原文地址:https://www.cnblogs.com/NeilZhang/p/7979629.html
Copyright © 2020-2023  润新知