美文网首页
单例模式的几种写法

单例模式的几种写法

作者: 小杰的快乐时光 | 来源:发表于2018-08-18 21:01 被阅读0次

    (1)饿汉模式

    public class Singleton{
        private Singleton(){};
        private static final Singleton INSTANCE = new Singleton();
        public static Singleton getInstance(){
            renturn INSTANCE;
        }
    }
    

    分析: 第一次加载到内存中的就会被初始化 ,并对外提供一个获取该实例对象的方法,优点:立即执行初始化,线程安全。缺点:会受到反射,序列化影响,浪费资源

    (2)懒汉模式:线程不安全,单线程下只在第一次调用时才会实例化

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

    懒汉加锁模式(1):线程安全,但是每次调用都会加锁,效率很低

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

    双重检查锁的懒汉单例模式(2):保证线程安全,只有在singleton为null时才会加锁,优化锁带来的消耗

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

    (3)静态内部类延迟初始化,线程安全

    public class Singleton {
       private Singleton(){};
       private static class initInstance{
          private static final Singleton SINGLETON = new Singleton();
       }
       public static Singleton getSingleton(){
          return initInstance.SINGLETON;
       }
    }
    

    分析:根据JVM规范,当第一次调用Singleleton.getInstance( )时,Singleleton被首次调用,JVM对其进行初始化操作,此时不会调用Singleleton的构造方法。接下来会调用getInstance()方法,又首次主动使用initInstance()内部类,所以JVM会对initInstance()进行初始化操作,在这个初始化操作中,静态常量INSTANCE被赋值时才调用Singleton()构造方法,完成实例化并返回该对象。
    当再次调用Singleleton.getInstance( )时,因为已经进行过初始化操作了,就不会再进行初始化操作,直接返回INSTANCE常量。

    解决反射与序列化的影响思路:(以饿汉单例模式为例)

    public class Singleton{
       private Singleton(){
          //在私有的无参构造器中进行判断,这样可以解决反射问题
          if (INSTANCE != null){
             throw new RuntimeException("禁止反射创建单例模式");
          }
       };
       private static final Singleton INSTANCE = new Singleton();
       public static Singleton getInstance(){
          return INSTANCE;
       }
       //添加readResolve()方法可以防止序列化问题
       private Object readResolve() throws ObjectStreamException{
          return INSTANCE;
       }
    }
    

    枚举类单例模式:可以满足线程安全,天生防止序列化生成新的实例,防止反射攻击

    public class Singleton{}
    
    public enum SingletonEnum{
        INSTANCE;
        private Singleton singleton = null;
        private SingletonEnum(){
            singleton = new Singleton();
        }
        public Singleton getInstance(){
            return singleton;
        }
    }
    
    调用:
    Singleton singleton = SingletonEnum.INSTANCE.getInstance();
    

    Enum是一个普通的类,继承自Java.lang.Enum类

    public enum SingletonEnum{
        INSTANCE;
    }
    

    将上面枚举编译后的字节码反编译,得到如下代码:

    public abstract class SingletonEnum extends Enum<SingletonEnum>{
        public static final SingletonEnum INSTANCE;
        public static SIngletonEnum[]values();
        public static SingletonEnum valuesof(String s);
        static{};
    }
    

    通过反编译后得到的代码可知:
    INSTANCE 被声明为static,虚拟机会保证一个类的<clinit>() 方法在多线程环境中被正确的加锁、同步。所以,枚举实现是在实例化时是线程安全。

    接下来再看序列化的问题
    在Java的规范中,每一个枚举类型及其定义的枚举变量在JVM中都是唯一的,在枚举类型的序列化与反序列化中,JVM做了一些特殊的规定:在序列化时,Java仅仅只是将枚举对象的name属性输出到结果中,反序列化时则是通过Java.lang.Enum的valuesof()方法根据名称获取枚举对象。
    比如:

    public enum SingletonEnum{
        INSTANCE;
    }
    

    上面枚举类在序列化时,只将 INSTANCE 这个名称输出,反序列化时,再通过这个名称,寻找对应的枚举类型,因此反序列化后的实例与之前被序列化的对象实例相同

    接下来看反射的问题
    枚举单例

    public enum Singleton {
    INSTANCE {
    
    @Override
    protected void read() {
    System.out.println("read");
    }
    
    @Override
    protected void write() {
    System.out.println("write");
    }
    
    };
    protected abstract void read();
    protected abstract void write();
    }
    

    反编译后

    public abstract class Singleton extends Enum
    {
    
        private Singleton(String s, int i)
        {
            super(s, i);
        }
    
        protected abstract void read();
    
        protected abstract void write();
    
        public static Singleton[] values()
        {
            Singleton asingleton[];
            int i;
            Singleton asingleton1[];
            System.arraycopy(asingleton = ENUM$VALUES, 0, asingleton1 = new Singleton[i = asingleton.length], 0, i);
            return asingleton1;
        }
    
        public static Singleton valueOf(String s)
        {
            return (Singleton)Enum.valueOf(singleton/Singleton, s);
        }
    
        Singleton(String s, int i, Singleton singleton)
        {
            this(s, i);
        }
    
        public static final Singleton INSTANCE;
        private static final Singleton ENUM$VALUES[];
    
        static
        {
            INSTANCE = new Singleton("INSTANCE", 0) {
    
                protected void read()
                {
                    System.out.println("read");
                }
    
                protected void write()
                {
                    System.out.println("write");
                }
    
            };
            ENUM$VALUES = (new Singleton[] {
                INSTANCE
            });
        }
    }
    

    ①类的修饰为abstract,没法实例化
    ②在static中完成实例化的,所以线程安全

    从源码上来看
    java.lang.reflect 中的 Constructor 的newInstance中规定了不准通过反射创建枚举对象
    (反射在通过newInstance创建对象时,会检查该类是否是枚举类,如果是,则抛出异常,反射失败。)

    Constructor 的newInstance.png

    参考文章:
    singleton模式四种线程安全的实现

    相关文章

      网友评论

          本文标题:单例模式的几种写法

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