美文网首页
单例模式整理

单例模式整理

作者: 修伊Dal | 来源:发表于2020-05-28 18:14 被阅读0次

    单例模式整理

    1简介

    单例一般实现只需要一个类就可以完成,在这个类中,确保此类只有一个实例化,自行实例化并且提供一个访问它的全局公有静态方法。

    使用场景

    1. 产生某种对象会消耗过多的资源,因此避免创建和销毁该对象时对资源浪费,如:

    对数据库的访问,访问IO,线程池,网络请求等

    2.有且只有一个的类型对象。这种对象如果创建多个实例,可能导致程序行为异常,资源使用过量,结果不一致等问题。如果有一个不进行版本控制的文件,多人对它同时操作,那么必然会有修改被覆盖的情况。如:

    窗口管理器或文件系统,计时工具或id(序号)生成器,缓存,处理偏好设置或注册表的对象,日志对象。

    优点

    可以减少系统的内存开支,减少系统性能开销,避免对资源的多重占用,重复操作。

    缺点

    拓展困难(不适合被继承),容易引发内存泄漏,测试困难,一定程度上违背了单一职责原则,进程被杀时可能有状态不一致的问题。

    注意点

    1. 构造方法为private的,这样除了它本身,不会被其他类实例化
    2. 通过一个静态方法或变量提供全局获取单例对象的访问点
    3. 防止唯一性不会被一些原因失效(如多线程,序列化,反射等,这在下面讲解)
    4. 单例即使一直没人用,也占据着内存,那些不是频繁创建和销毁,且创建和销毁也不会消耗太多资源的情况,不要滥用

    2单例的实现

    多线程安全

    单例要最先确保它的唯一性,但是有很多因素会导致唯一性失效,如多线程,序列化,反射,克隆等,更特殊的有分布式系统,多个类加载器等。这些原因会破坏单例

    其中,多线程在现在业务中普遍使用,所以实际开发中如果不能做到多线程安全的单例模式都应该被弃用。

    但下面为了整理线程不安全的还是会贴出来(线程不安全的会标注)

    懒汉式

    线程不安全的懒汉式

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

    在多线程下,可能会发生一个线程通过并进入if(singleton == null)的判断语句块,却还没来得及创建实例,这时另一个线程也通过并进入if(singleton == null)的判断语句块的情况,最终两个线程都能成功创建实例。多线程下实际上却创建了多个实例,破坏了唯一性。

    为了能更直观看到效果,对 getInstance 做了一些改造,在创建实例前睡眠300ms。

    public static MySingleton getInstance() {
        try {
            if (singleton == null) {
                // 模拟创建实例前一些准备性的耗时工作
                Thread.sleep(300);
                singleton = new MySingleton();
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return singleton;
    }
    

    实验代码:

    public class MyThread extends Thread{
    
        @Override
        public void run() {
            System.out.println(MySingleton.getInstance().hashCode());
        }
    
        public static void main(String[] args) {
            MyThread[] myThreads = new MyThread[10];
            for(int i = 0; i < myThreads.length; i++) {
                myThreads[i] = new MyThread();
            }
    
            for (MyThread myThread : myThreads) {
                myThread.start();
            }
        }
    }
    /*
    结果:
    com.singleton.MySingleton@9a662fb
    com.singleton.MySingleton@6299d4f3
    com.singleton.MySingleton@9a662fb
    com.singleton.MySingleton@9a662fb
    com.singleton.MySingleton@9a662fb
    com.singleton.MySingleton@25e63a54
    com.singleton.MySingleton@75c85ca
    com.singleton.MySingleton@7042b8f1
    com.singleton.MySingleton@9a662fb
    com.singleton.MySingleton@9a662fb
    */
    

    可以看出有一些线程创建的对象是不同的,说明多线程下不是单例。

    synchronized修饰的懒汉式

    当然我们可以通过synchronized修饰符来解决线程不安全的问题。

    下面是同步方法的懒汉式:

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

    通过为getInstance方法增加synchronized修饰符,迫使每个线程在进入方法前都要等待别的线程离开该方法,这样不会有两个线程同时进入该方法实例,从而保证单例的有效,但除了创建实例化,其他并不是必须要同步的,全部代码同步会使效率低下。如果把同步方法改成同步代码块,虽比之前好,但实际开发中效率同样很低下。

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

    注意,如果只同步实例化的代码块依旧是线程不安全的。

    public static MySingleton getInstance() {
        if (singleton == null) {
            // 仅同步实例化的代码块,线程不安全
            synchronized(MySingleton.class) {
                singleton = new MySingleton();
            }
        }
        return singleton;
    }
    

    就如同简单的懒汉式一样,会产生多个实例。那么如果我们如果在代码块中再增加一个非空判断,是不是就可以了呢。这就是"双重检查锁(Double Check Lock(DCL))"。

    双重检查锁(DCL)

    public class MySingleton {
    
        // volatile很重要,避免指令重排序造成DCL失效
        private static volatile MySingleton singleton;
    
        private MySingleton(){}
    
        public static MySingleton getInstance() {
            // 第一次check避免不必要的同步
            if (singleton == null) {
                synchronized (MySingleton.class) {
                    // 第二次check保证线程安全
                    if (singleton == null) {
                        singleton = new MySingleton();
                    }
                }
            }
            return singleton;
        }
    }
    

    这样保证了线程安全又在多线程下比同步方法的方式效率高,Spring中依赖注入单例bean中就有使用DCL的模式来创建单例bean(见附录)。

    如果假设有一百个线程,那么其耗时如下:

    单例模式 第一次运行最大耗时 继续运行最大耗时
    (同步方法)懒汉式 100*(同步判断时间+if判断) 100*(同步判断时间+if判断)
    DCL 100*(同步判断时间+if判断) + if判断 if判断

    同步方法导致每个线程都要进行同步判断+if判断,继续运行一就如此。

    而DCL假设最差的情况一百个线程同时进行if判断,这样第一个if只需要一个判断时间,但相对的,后面的同步判断和第二个if判断也要进行100次。第一次运行可能耗时会多于同步方法,但如果继续运行。由于已经实例化,因此后面只会进行第一个if判断,耗时相比同步方法就会大大减少。

    再提一下volatile这个修饰符,这是必要的,但在网上还是会看到缺少这个修饰符的DCL。

    volatile阻止了jvm的指令重排序。理想的来说,指令是顺序执行,但顺序执行往往以为低效,所以现在多处理器架构往往会对指令重排序。

    一个对象的实例化可以分成以下三个步骤:

    1. 分配内存空间
    2. 初始化对象
    3. 将对象指向刚分配的内存空间。

    一般来说是1->2->3的执行顺序,但jvm为了效率也会以1->3->2的顺序,如果使用1->3->2的顺序,那么在多线程下DCL可能会出现以下情况:

    时间 线程1 线程2
    t1 检查到 singleton 为空(第一个if)
    t2 获取锁
    t3 再次检查到 singleton 为空(第二个if)
    t4 singleton 分配内存空间
    t5 singleton 指向刚分配的内存空间
    t6 检查到 singleton不为空
    t7 访问 singleton(但此时 singleton 并没有初始化)
    t8 初始化 singleton

    结果就是线程2在t7中访问的 singleton 是还没初始化的对象,结果报错,DCL失效。(不过本人实际试了好几次并没有试出来,网上也没查到DCL必会因重排序而报错的代码,如果有的话请给出)。

    至于为什么volatile能阻止到重排序,这就涉及到“内存屏障”,就不细说了。

    内部静态类

    最后再说一下 “静态内部类”实现的单例模式:

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

    这其实也属于懒汉式,MySingleton被装载时,只要SingletonHolder没有被主动调用,INSTANCE是被会实例化的。只有显式调用getInstance,才会实例化对象。这种懒加载是懒汉式的特性。

    静态内部类是定义静态的成员变量,依靠JVM的类加载机制保证static初始化状态实例的唯一性。因为ClassLoader的线程安全机制,ClassLoader在加载类的时候调用loadClass方法,这个loadClass方法使用了synchronized代码块,所以在整个类加载过程是线程安全的,类加载中对象的创建也是线程安全的。

    protected Class<?> loadClass(String name, boolean resolve)
            throws ClassNotFoundException
        {
            synchronized (getClassLoadingLock(name)) {
                // 此处省略部分代码
            }
        }
    

    懒汉式里面推荐DCL静态内部类DCL在低于JDK1.5的版本时会失效(因为volatile在1.5以后才开始正确发挥出它的效果,不过现在基本最低都是1.8了吧),而静态内部类任意JDK版本都可以使用,除此以外两者都有懒加载,线程安全,效率较高的特点。

    饿汉式

    饿汉式与懒汉式的区别在于加载时机。饿汉式在类创建的同时实例化单例,而懒汉式是在第一次调用的时候实例初始化的,以后每次获取实例时也要先判断是否以及实例初始化。因此饿汉式反应速度更快,但会一直占用资源,而懒汉式加载会有延迟,但资源利用率高。这也是牺牲空间换时间或者牺牲时间换空间的问题。下面介绍三种饿汉式。

    公有静态域的饿汉式

    // Singleton with public final field
    public class MySingleton {
        // 注意是public修饰符。同时final关键字保证总是同一个对象引用
        public static final MySingleton INSTANCE = new MySingleton();
        private MySingleton(){}
    }
    

    优点是简单,无需公有成员方法getInstance,就可以直接拿到INSTANCE实例。

    公有静态工厂方法的饿汉式

    // Singleton with static factory
    public class MySingleton {
        private static final MySingleton INSTANCE = new MySingleton();
        private MySingleton(){}
    
        public static MySingleton getInstance() {
            return INSTANCE;
        }
    }
    

    相比于第一种公有静态域写法,这个写法更加常见,也有很多优势。

    1. 更加灵活,我们可以在不改变API的情况下,只需改动getInstance的逻辑,就可以改成非单例,比如改成为每个调用该方法的线程返回一个唯一的实例。
    2. 如有应用程序需要,可以改编成一个泛型单例工厂(见附录)。
    3. 可以通过方法引用(::)提供方法,如Supplier<MySingleton> singletonSupplier = MySingleton::getInstance;

    但是除非满足以上任意一种优势,否则还是优先考虑公有域(public-field)的写法。

    公有静态工厂方法变种

    这个写法与前一个静态工厂写法基本没有差别,所以就不说了。

    public class MySingleton {
        private static MySingleton INSTANCE = null;
        static {
            INSTANCE = new MySingleton();
        }
        private MySingleton(){}
    
        public static MySingleton getInstance() {
            return INSTANCE;
        }
    }
    

    饿汉式都是线程安全的,原理与懒汉式的静态内部类一样,都是定义静态成员变量,利用ClassLoader的线程安全机制,保证在类加载中对象的创建是安全的。

    枚举

    public enum Singleton {
        INSTANCE;
        public void whateverMethod(){}
    }
    

    通过声明一个包含单个元素的枚举类型来实现单例,这是《Effective Java》中推荐的写法。比公有域方法更加简洁也更加安全(后面单例的破坏中会提到反序列化、反射和克隆会破坏单例,但是枚举方式提供了序列化机制可以防止多次实例化,枚举没有无参构造函数不用担心反射绕过限制,枚举也不支持克隆可以防止克隆的破坏,见附录)。引用《Effect Java》中的话:

    使用枚举实现单例的方法虽然还没有广泛采用,但是单元素的枚举类型已经成为大部分情况实现Singleton的最佳方法。

    不过如果要用单例类去扩展一个超类,而不是Enum时,就不适合用枚举方式。

    继续探究原理,我们将它反编译一下。

    /*
     * Decompiled with CFR 0.150.
     */
    package com.singleton;
    
    public final class Singleton
    extends Enum<Singleton> {
        public static final /* enum */ Singleton INSTANCE = new Singleton("INSTANCE", 0);
        private static final /* synthetic */ Singleton[] $VALUES;
    
        public static Singleton[] values() {
            return (Singleton[])$VALUES.clone();
        }
    
        public static Singleton valueOf(String name) {
            return Enum.valueOf(Singleton.class, name);
        }
    
        private Singleton(String string, int n) {
            super(string, n);
        }
    
        public void whateverMethod() {
        }
    
        static {
            $VALUES = new Singleton[]{INSTANCE};
        }
    }
    

    可以看出其实还是用静态的成员变量和静态代码块依靠类加载的线程安全机制保证单例的唯一性。

    容器管理

    public class MySingleton {
        // 线程是否安全取决于容器,这里HashMap是线程不安全的,
        // 如果想要线程安全可以采用ConcurrentHashMap
        private static Map<String, Object> singletonMap = new HashMap<>(16);
    
        private MySingleton(){}
    
        public static void pushInstance(String key, Object instance) {
            if (!singletonMap.containsKey(key)) {
                singletonMap.put(key, instance);
            }
        }
    
        public static Object getInstance(String key) {
            return singletonMap.get(key);
        }
    }
    

    在我们需要管理多个不同单例的时候就可以用容器来管理不同的单例。我们将可以一组单例放入Map容器中,使用时根据key值获取即可,这种方式隐藏了单例的实现同时还降低了耦合性。

    我们熟悉的spring就是采用ConcurrentHashMap来管理bean的。

    CAS

    public class MySingleton {
        private static final AtomicReference<MySingleton> INSTANCE = new AtomicReference<>();
    
        private MySingleton(){}
    
        public static MySingleton getInstance() {
            // 无限循环自旋
            while (true) {
                MySingleton singleton = INSTANCE.get();
                if (null != singleton) {
                    return singleton;
                }
                singleton = new MySingleton();
                if (INSTANCE.compareAndSet(null, singleton)) {
                    return singleton;
                }
            }
        }
    }
    

    CAS采用的并不是传统的锁来控制线程安全,而是一种忙等待,也就是自旋。优点是相比传统的锁,自旋没有线程切换的消耗,但是对CPU消耗巨大(因为一直在死循环),你可以理解多个线程在实例化那一步创建了多个对象,但实际符合CAS操作结果的只有一个对象。但是,因为在实例化那一步多个线程可能会创建多个对象,这会导致内存溢出。

    ThreadLoacl伪单例

    public class MySingleton {
        private static final ThreadLocal<MySingleton> MY_SINGLETON =
                ThreadLocal.withInitial(MySingleton::new);
    
        private MySingleton() {}
    
        public static MySingleton getInstance() {
            return MY_SINGLETON.get();
        }
    }
    

    严格意义上这不能算单例,ThreadLocal为每一个线程提供一个独立的变量副本,多个线程就提供多个不同的变量副本,相互之间不影响。但这个多线程下的不同对象与之前多线程对单例的破坏又是不同的。

    下面的代码可以直观的看到ThreadLocal创建了多个独立的变量副本。

    public class MyThread extends Thread{
    
        @Override
        public void run() {
            System.out.println(this.getName() + "+" + MySingleton.getInstance());
        }
    
        public static void main(String[] args) {
            for (int i = 0; i < 5; i++) {
                System.out.println("mainThread+" + MySingleton.getInstance());
            }
    
            MyThread[] myThreads = new MyThread[5];
            for(int i = 0; i < myThreads.length; i++) {
                myThreads[i] = new MyThread();
            }
    
            for (MyThread myThread : myThreads) {
                myThread.start();
            }
        }
    }
    /*
    结果:
    mainThread+com.singleton.MySingleton@7c3df479
    mainThread+com.singleton.MySingleton@7c3df479
    mainThread+com.singleton.MySingleton@7c3df479
    mainThread+com.singleton.MySingleton@7c3df479
    mainThread+com.singleton.MySingleton@7c3df479
    Thread-0+com.singleton.MySingleton@2b3351f2
    Thread-1+com.singleton.MySingleton@48bcc212
    Thread-2+com.singleton.MySingleton@6afa9656
    Thread-3+com.singleton.MySingleton@500d9cf1
    Thread-4+com.singleton.MySingleton@2efdec40
    */
    

    前5个是同一个线程下,所以都是同一个对象。但后面5个不同的线程明显是不同的5个对象。

    3单例的破坏

    前面提到过,单例会被很多原因破坏唯一性。除了最突出的多线程外,还有反射,序列化,克隆,甚至分布式,多个类加载器的情况下。

    反射

    在单例中,我们会把构造函数私有防止被调用创建新的对象。而反射可以通过setAccessible(true)来绕过private限制,从而调用到构造函数。

    public class MySingleton {
    
        public static final MySingleton INSTANCE = new MySingleton();
    
        private MySingleton(){}
    
        public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
            Constructor<MySingleton> constructor = MySingleton.class.getDeclaredConstructor();
            constructor.setAccessible(true);
            MySingleton newInstance = constructor.newInstance();
            System.out.println(INSTANCE == newInstance);
        }
    }
    /*
    结果:
    false
    */
    

    针对反射的破坏,我们可以改造构造函数,在第二次调用创建对象时抛出异常。

    private MySingleton(){
        // 这里也可以依靠设置成员变量来判断,比如设置一个代表是否已经创建对象的boolean值flag,每次调用构造函数判断是否已经为true
        // 又比如设置一个计数器count,默认是0,创建完对象就+1,每次调用构造函数就可以判断是否大于0
        if (null != INSTANCE) {
            throw new RuntimeException("Cannot construct a Singleton more than once");
        }
    }
    /*
    结果:
    Exception in thread "main" java.lang.reflect.InvocationTargetException
        ...
    Caused by: java.lang.RuntimeException: Cannot construct a Singleton more than once
        ... 
    */  
    

    序列化

    为了实现单例类的可序列化,光光声明实现了Serializable接口可是不行的。下面的代码结果就展示了每次反序列化时,都会产生一个新的实例,这与单例的唯一性违背。

    public class MySingleton implements Serializable {
    
        public static final MySingleton INSTANCE = new MySingleton();
        private static final long serialVersionUID = 1617768107171019689L;
    
        private MySingleton(){}
    
        public static void main(String[] args) throws IOException, ClassNotFoundException {
            File f = new File("tempFile.txt");
            ObjectOutputStream oss = new ObjectOutputStream(new FileOutputStream(f));
            oss.writeObject(INSTANCE);
            oss.close();
            ObjectInputStream ois = new ObjectInputStream(new FileInputStream(f));
            MySingleton newInstance = (MySingleton)ois.readObject();
            ois.close();
            System.out.println("Is old instance by deserialize: " + (INSTANCE == newInstance));
        }
    }
    /* 
    结果:
    Is old instance by deserialize: false
    */
    

    对此如果想避免反序列化创建新的对象,那么在实现Serializable接口的同时增加一个readResolve方法,这个方法返回单例对象(如果全局访问单例对象是通过getInstance静态方法,则readResolve返回getInstance。因为这里测试用的是静态公有域的饿汉式,所以返回单例对象INSTANCE)。

    private Object readResolve() {
        return INSTANCE;
    }
    /* 
    结果:
    Is old instance by deserialize: true
    */
    

    这是一个很特别的方法。在上面代码中ObjectInputStream从流中读取数据时,调用了readObject方法。在这个readObject方法中,有这么一个方法调用顺序。

    readObject -> readObject0 -> readOrdinaryObject ->checkResolve

    其中最重要的是readOrdinaryObject这一方法,下面是这方法的部分源码。

    private Object readOrdinaryObject(boolean unshared)
        throws IOException
        {
            // 此处省略部分代码
    
            Object obj;
            try {
                // isInstantiable,如果一个类实现了Serializable或者externalizable接口并且运行时能实例化,则返回true
                // newInstance,如果一个类实现Serializable,那么通过反射的方式调用第一个不可序列化的超类的无参构造函数来创建新的对象
                // 这里的单例类的第一个不可序列化的超类是Object类,所示调用的是Object的无参构造函数
                // 因此这一步obj其实是一个新的对象
                obj = desc.isInstantiable() ? desc.newInstance() : null;
            } catch (Exception ex) {
                throw (IOException) new InvalidClassException(
                    desc.forClass().getName(),
                    "unable to create instance").initCause(ex);
            }
            
            // 此处省略部分代码
    
            // hasReadResolveMethod:会判断你的序列化类是否有readResolve这一方法,在未添加此方法之前,这边的判断是false,因此会跳过if语句块中的代码,把新的对象直接返回
            if (obj != null &&
                handles.lookupException(passHandle) == null &&
                desc.hasReadResolveMethod())
            {
                // invokeReadResolve:通过反射调用序列化类的readResolve方法,在这里就是MySingleton.readResolve,rep因此就是单例INSTANCE
                Object rep = desc.invokeReadResolve(obj);
                if (unshared && rep.getClass().isArray()) {
                    rep = cloneArray(rep);
                }
                if (rep != obj) {
                    // Filter the replacement object
                    if (rep != null) {
                        if (rep.getClass().isArray()) {
                            filterCheck(rep.getClass(), Array.getLength(rep));
                        } else {
                            filterCheck(rep.getClass(), -1);
                        }
                    }
                    // rep赋值给obj,也就是说obj在这一步从新的对象变成序列化前的单例对象
                    handles.setObject(passHandle, obj = rep);
                }
            }
    
            return obj;
        }
    

    其实在 obj = desc.isInstantiable() ? desc.newInstance() : null;这一步就可以解释为什么序列化可以破坏单例了。

    因为在序列化过程中会通过反射调用第一个不可序列化的超类的无参构造函数来创建了一个新的对象。

    因此其实还是反射的对单例的破坏,但是注意是调用第一个不可序列化父类的无参构造函数,直接套用上面解决反射的破坏问题的解决方法(序列化类无参构造函数抛出异常)是不行的,因为调用的不是同一个构造函数。

    克隆

    除了反射和序列化,克隆也会破坏单例。

    public class MySingleton implements Cloneable{
    
        public static final MySingleton INSTANCE = new MySingleton();
    
        private MySingleton(){}
    
        public static void main(String[] args) throws CloneNotSupportedException {
            MySingleton cloneInstance = (MySingleton) INSTANCE.clone();
            System.out.println("Is old instance by clone: " + (INSTANCE == cloneInstance));
        }
    }
    /*
    结果:
    Is old instance by clone: false
    */
    

    我们无法直接禁止单例实现Cloneable接口,一些单例列会继承其他一些父类,这些父类可能是实现Cloneable接口。因此与其不合理的禁止,不如我们重写clone方法,使其抛出异常。

    @Override
    protected Object clone() throws CloneNotSupportedException {
        throw new CloneNotSupportedException();
    }
    /*
    结果:
    Exception in thread "main" java.lang.CloneNotSupportedException
    */
    

    多个类加载器

    在前面静态内部类,饿汉式,枚举中提到过他们是依靠类加载器的线程安全机制。但一些工程下(比如分布式或集群系统)会有多个类加载器,这时候一个类可能会被加载多次,就会有多个单例,唯一性就失效了。这时候就要指定同一个类加载器加载。

    附录

    泛型单例工厂

    有时候我们会需要创建一个可以适用于不同类型但类型之间又有共同行为的对象,这时候我们可以采用这种模式。因为泛型通过擦除让每个类型参数使用单个对象,静态工厂方法可以重复分发对象给每个类型参数,这样组合使用就实现了单例和代码复用。这种模式常用于函数对象,如Collections.reverseOrder, 有时候也用于像Collections.emptySet的集合.

    // <<Effective Java>>中的举例,编写一个恒等函数分发器
    public class MySingleton {
        // (t) -> t:恒等函数,这是这个例子中不同类型参数拥有的共同行为
        // 同时这里重写了UnaryOperator实现接口Function的apply方法
        private static UnaryOperator<Object> IDENTITY_FN = (t) -> t;
    
        // 恒等函数返回未被修改的参数,因此无论t是什么,转换都是类型安全的
        @SuppressWarnings("unchecked")
        public static <T> UnaryOperator<T> identityFunction() {
            return (UnaryOperator<T>)IDENTITY_FN;
        }
    
        public static void main(String[] args) {
            String[] strings = {"jute", "hemp", "nylon"};
            UnaryOperator<String> sameString = identityFunction();
            for (String s : strings) {
                System.out.println(sameString.apply(s));
            }
    
            Number[] numbers = {1,2.0,3L};
            UnaryOperator<Number> sameNumber = identityFunction();
            for (Number n : numbers) {
                System.out.println(sameNumber.apply(n));
            }
        }
    }
    /*
    结果:
    jute
    hemp
    nylon
    1
    2.0
    3
    */
    

    Spring创建单例bean

    Spring中默认依赖注入的bean中是单例的,在调用AbstractBeanFactorygetBean依赖注入bean时,getBeandoGetBean会调用getSingleton进行bean的创建。

    我们看一下源码。

    @Override
    @Nullable
    public Object getSingleton(String beanName) {
        return getSingleton(beanName, true);
    }
    
    @Nullable
    protected Object getSingleton(String beanName, boolean allowEarlyReference) {
        Object singletonObject = this.singletonObjects.get(beanName);
        // 这里开始第一个if非空判断
        if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
            // 这里同步代码块
            synchronized (this.singletonObjects) {
                singletonObject = this.earlySingletonObjects.get(beanName);
                // 这里第二个if非空判断
                if (singletonObject == null && allowEarlyReference) {
                    ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);
                    if (singletonFactory != null) {
                        singletonObject = singletonFactory.getObject();
                        this.earlySingletonObjects.put(beanName, singletonObject);
                        this.singletonFactories.remove(beanName);
                    }
                }
            }
        }
        return singletonObject;
    }
    

    我们可以清楚的看到有一个明显的DCL方式。

    为什么枚举是大部分情况最佳单例方法

    首先是简单,从实现那一章来看,这是最简单的实现方法。

    其次就拿单例的破坏的因素一个个来说,

    1. 从之前反编译可以看出是设置静态公有域和代码块依靠类加载的线程安全机制保证线程安全。
    2. 本身不支持clone不用担心克隆的破坏。
    3. 枚举没有无参构造函数,不用担心反射绕过private限制调用。反射也不支持Enum创建对象。
    4. 序列化底层不依靠反射,自己的序列化机制也可以防止多次实例化

    前两个就不说了,从反射开始说起吧。

    枚举对于反射的避免

    我们拿上一章单例的破坏中的反射代码来测试。

    public enum Singleton {
        INSTANCE;
        public void whateverMethod(){}
    }
    public class SingletonTest {
        public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
            Constructor<Singleton> constructor = Singleton.class.getDeclaredConstructor();
            constructor.setAccessible(true);
            Singleton newInstance = constructor.newInstance();
            System.out.println(Singleton.INSTANCE == newInstance);
        }
    }
    /*
    结果:
    Exception in thread "main" java.lang.NoSuchMethodException: com.singleton.Singleton.<init>()
        at java.lang.Class.getConstructor0(Class.java:3082)
        at java.lang.Class.getDeclaredConstructor(Class.java:2178)
        at com.singleton.SingletonTest.main(SingletonTest.java:11)
    */
    

    嗯,抛出了NoSuchMethodException的异常,从异常中可以看到是getDeclaredConstructor调用getConstructor0报错,我们debug一下.

    public Constructor<T> getDeclaredConstructor(Class<?>... parameterTypes)
        throws NoSuchMethodException, SecurityException {
        checkMemberAccess(Member.DECLARED, Reflection.getCallerClass(), true);
        return getConstructor0(parameterTypes, Member.DECLARED);
    }
    private Constructor<T> getConstructor0(Class<?>[] parameterTypes,
                                            int which) throws NoSuchMethodException
        {
            // 获取构造函数
            Constructor<T>[] constructors = privateGetDeclaredConstructors((which == Member.PUBLIC));
            for (Constructor<T> constructor : constructors) {
                // parameterTypes:getDeclaredConstructor入参,constructor.getParameterTypes()构造函数参数,这是入参与构造函数参数匹配的方法,确认要调用那个构造函数
                if (arrayContentsEq(parameterTypes,
                                    constructor.getParameterTypes())) {
                    return getReflectionFactory().copyConstructor(constructor);
                }
            }
            throw new NoSuchMethodException(getName() + ".<init>" + argumentTypesToString(parameterTypes));
        }
    

    在反编译的代码中我们可以看到枚举只有private Singleton(String string, int n)这一个构造函数,它的参数数组是[String,int]。我们传进来入参是空数组,因此匹配不上,arrayContentsEq返回false,直接抛出异常。

    那我们把测试代码改为Constructor<Singleton> constructor = Singleton.class.getDeclaredConstructor(String.class,int.class);指定用那个构造器呢?

    public class SingletonTest {
        public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
            Constructor<Singleton> constructor = Singleton.class.getDeclaredConstructor(String.class, int.class);
            constructor.setAccessible(true);
            Singleton newInstance = constructor.newInstance();
            System.out.println(Singleton.INSTANCE == newInstance);
        }
    }
    /*
    结果:
    Exception in thread "main" java.lang.IllegalArgumentException: Cannot reflectively create enum objects
        at java.lang.reflect.Constructor.newInstance(Constructor.java:417)
        at com.singleton.SingletonTest.main(SingletonTest.java:13)
    */
    

    嗯,又是异常,只不过换了一个。不过反射包下的Constructor.newInstance这个是不是很熟悉。其实在单例的破坏那一章反射和序列化创建新对象破坏单例时底层也是用的这一个方法。

    // Constructor.newInstance
    public T newInstance(Object ... initargs)
            throws InstantiationException, IllegalAccessException,
                   IllegalArgumentException, InvocationTargetException
        {
            // 此处省略部分代码
            // 这里判断是否是枚举,如果是枚举则抛出异常。
            if ((clazz.getModifiers() & Modifier.ENUM) != 0)
                throw new IllegalArgumentException("Cannot reflectively create enum objects");
            // 此处省略部分代码
        }
    

    也就是说反射并不支持调用枚举的构造函数。

    枚举对于序列化的避免

    在oracle的文档中有指出,枚举的序列化与普通的java类序列化是不同的。 Serialization of Enum Constants

    枚举序列化.png

    大意是枚举序列化只是将name写入,反序列化是通过父类EnumvalueOf方法根据name对象来查找对象,与普通的java反序列化通过反射创建新对象不同。同时官方是不允许对枚举序列化自定义的,所以writeObjectreadObjectreadObjectNoDatawriteReplacereadResolve等方法会被忽略,serialPersistentFields or serialVersionUID也是被忽略的,只定义为0L。 这样就保证了枚举在序列化中一定是安全的。

    不过枚举也不是完全安全的,既然是依靠类加载机制来保证线程安全,那么在多个类加载器的情况下就可能会失效,参考文章中有一篇是本人看到的失效案例,不过本人没试过,经供参考。

    参考文章

    潜谈单例模式

    《Effective Java》

    为什么我强烈建议大家使用枚举来实现单例

    枚举实现的单例模式真的就万无一失了吗?

    相关文章

      网友评论

          本文标题:单例模式整理

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