美文网首页
单例的实现方法总结

单例的实现方法总结

作者: 白花蛇草可乐 | 来源:发表于2019-09-28 16:03 被阅读0次

    单例的实现方法总结

    以下的内容不涉及基础,比如什么是单例?JVM类加载顺序?等等。

    仅仅是对所有单例的实现方法进行汇总。

    一、最经典的饿汉模式实现方式

    public class Singleton1 {
        private final static Singleton1 INSTANCE = new Singleton1();
        private Singleton1(){
        }
        public static Singleton1 getInstance() {
            return INSTANCE;
        }
    }
    

    另外一个变种的实现方法,是将静态成员改为静态代码块

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

    不管怎么写,本质上利用的都是“类的初始化过程(包含静态成员赋值,以及静态代码块的执行),只在类被加载到内存时执行一次”这一特性。

    1. 优点

    • 由于其原理,天然就是线程安全的
    • 结构简单理解容易
    1. 缺点

    • 相对于懒汉模式,饿汉模式最麻烦的地方在于,如果创建单例类的对象要依赖参数或者外部配置文件的话,也就是说,业务场景需要在调用getInstance方法时传入参数,决定用何种方式创建单例实例的话,饿汉模式就无法使用了。

    二、懒汉模式实现方法

    public class Singleton2 {
        private static Singleton2 instance;
        private Singleton2 (){
        }
        public static synchronized Singleton2 getInstance() {
            if (instance == null) {
                instance = new Singleton2();
            }
            return instance;
        }
    }
    
    1. 优点

    • 在必须使用延迟加载的场景下,替代饿汉模式
    1. 缺点

    • 最重要的一点,为了确保线程安全,必须使用synchronized关键字进行同步,影响性能。

    三、双重检查方法

    双重检查其实就是对于懒汉模式的一种性能改进,减小了synchronized关键字锁定的代码块范围。
    第二重检查的作用是:防止有别的线程,在第一重检查和拿锁之间创建了单例实例。

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

    四、静态内部类方法

    这种方法,也是利用了类加载的特性,在getInstance()方法调用静态内部类的静态成员变量时,静态内部类SingletonHolder才会被初始化,创建单例实例。
    (复习:使用 Class.staticMember 方式引用类的静态成员变量,属于对类进行主动引用,在这种情况下会触发类加载的初始化过程)

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

    优点

    • 延迟加载
    • 无锁,没有性能损耗
    • 天然线程安全

    五、枚举类

    这是一种最简洁但是最难理解的单例实现方法。但是《Effective Java》评价这是实现单例的最佳方法(参看该书第3条)

    public enum Singleton5 {
        /**
         * 枚举实现单例
         */
        INSTANCE;
        public void businessMethod() {
        }
    }  
    

    其调用方法如下:

    Singleton5.INSTANCE.businessMethod()
    

    下面解释枚举类为什么能实现单例。

    1. 首先对于Singleton5编译好的class进行反编译

    因为enum只是一个关键字,不是超类或者其他能看到源码的东西。因此利用反编译的手段来确认内部实现(可以使用jad等工具)。

    package singleton;
    
    
    public final class Singleton5 extends Enum
    {
    
        public static Singleton5[] values()
        {
            return (Singleton5[])$VALUES.clone();
        }
    
        public static Singleton5 valueOf(String name)
        {
            return (Singleton5)Enum.valueOf(singleton/Singleton5, name);
        }
    
        private Singleton5(String s, int i)
        {
            super(s, i);
        }
    
        public void businessMethod()
        {
        }
    
        public static final Singleton5 INSTANCE;
        private static final Singleton5 $VALUES[];
    
        static 
        {
            INSTANCE = new Singleton5("INSTANCE", 0);
            $VALUES = (new Singleton5[] {
                INSTANCE
            });
        }
    }
    
    
    1. 枚举如何保证线程安全

    可以看到,使用enum关键字的话,实际会生成一个继承了Enum,并且final的类。

    public final class Singleton5 extends Enum
    

    注意下面的这一段:

        public static final Singleton5 INSTANCE;
        private static final Singleton5 $VALUES[];
    
        static 
        {
            INSTANCE = new Singleton5("INSTANCE", 0);
            $VALUES = (new Singleton5[] {
                INSTANCE
            });
        }
    
    • 根据类加载过程,在“链接”的“准备”阶段,静态且final的静态成员INSTANCE被加载到了方法区(如果这里有赋值操作的话就有值了,不会等到初始化阶段,这是final与其他不同的地方)。
    • 到了初始化阶段,会执行静态代码块内的内容,开辟内存空间存放单例实例,并将地址赋值给INSTANCE。
    • 类加载过程是只会执行一次的,所以本质上还是利用jvm规定的类加载过程,形成了天然的线程安全
    • 另外,构造函数 new Singleton5("INSTANCE", 0) 实际上调用是 super,也就是 Enum 类的构造函数,第一个参数是枚举名称(name),第二个参数是顺序(ordinal)。
    1. 解决反序列化问题

    • 前面除了枚举类以外的单例的实现方法,都有一个弱点,如果需要进行序列化的话(implements Serializable),那么在反序列化的时候,每次调用readObject()方法都会生成一个不同于原单例的新实例,单例失效。
    • 对于枚举,为了保证枚举类型符合Java相关规范(JSR),每一个枚举类及其定义的枚举变量在JVM中都是唯一的,在枚举类型的序列化和反序列化上,Java有特殊处理
    • 序列化的时候,Java仅将枚举类的name属性输出到结果中,反序列化的时候通过Enum的valueOf方法来根据名字查找枚举对象。
        public static <T extends Enum<T>> T valueOf(Class<T> enumType,
                                                    String name) {
            T result = enumType.enumConstantDirectory().get(name);
            if (result != null)
                return result;
            if (name == null)
                throw new NullPointerException("Name is null");
            throw new IllegalArgumentException(
                "No enum constant " + enumType.getCanonicalName() + "." + name);
        }
    
    
    • 而valueOf()调用enumConstantDirectory(),继而调用getEnumConstantsShared(),可以看到里面实际调用的是反编译出来的那段代码里的value()方法,使用的实际上就是那个$VALUES[]。
        Map<String, T> enumConstantDirectory() {
            if (enumConstantDirectory == null) {
                T[] universe = getEnumConstantsShared();
                if (universe == null)
                    throw new IllegalArgumentException(
                        getName() + " is not an enum type");
                Map<String, T> m = new HashMap<>(2 * universe.length);
                for (T constant : universe)
                    m.put(((Enum<?>)constant).name(), constant);
                enumConstantDirectory = m;
            }
            return enumConstantDirectory;
        }
    
        T[] getEnumConstantsShared() {
            if (enumConstants == null) {
                if (!isEnum()) return null;
                try {
                    final Method values = getMethod("values");
                    java.security.AccessController.doPrivileged(
                        new java.security.PrivilegedAction<Void>() {
                            public Void run() {
                                    values.setAccessible(true);
                                    return null;
                                }
                            });
                    @SuppressWarnings("unchecked")
                    T[] temporaryConstants = (T[])values.invoke(null);
                    enumConstants = temporaryConstants;
                }
                // These can happen when users concoct enum-like classes
                // that don't comply with the enum spec.
                catch (InvocationTargetException | NoSuchMethodException |
                       IllegalAccessException ex) { return null; }
            }
            return enumConstants;
        }
    
    • 所以枚举即使被反序列化也不会创建对象。
    • 所以枚举即使被反序列化也不会创建对象。
    • 所以枚举即使被反序列化也不会创建对象。

    六、应对多个类加载器

    前面的所有方法都有一个共通的问题:被多个类加载器加载。

    这问题不算是钻牛角尖,一些热启动机制的框架,就是利用多个类加载器实现的,这时候确实有可能造成单例变成多例。

    在网上找到了一段代码来解决这个问题,就是增加下面这个私有静态类。

    原理是在被调用getClass方法时,直接利用自身原来的类加载器进行类加载,确保自始至终一直是同一个类加载器在加载单例类。

    private static Class getClass(String classname) throws ClassNotFoundException {     
          ClassLoader classLoader = Thread.currentThread().getContextClassLoader();     
          
          if(classLoader == null)     
             classLoader = Singleton.class.getClassLoader();     
          
          return (classLoader.loadClass(classname));     
       }     
    }  
    

    相关文章

      网友评论

          本文标题:单例的实现方法总结

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