美文网首页
设计模式——单例模式

设计模式——单例模式

作者: 张先森丶 | 来源:发表于2019-03-10 18:42 被阅读0次

    文章概要

    1、什么是单例
    2、为什么需要单例
    3、单例的优点和缺点
    4、单例的写法和比较
    5、序列化破坏单例
    6、反射破坏单例
    7、不使用synchronized和lock,如何实现一个线程安全的单例?
    8、JDK中的单例
    9、引用

    1、什么是单例

    单例是23中设计模式之一,保证一个类只有一个实例,并且对外提供统一的访问入口。是创建型模式。

    2、为什么需要单例

    我们都知道,类的对象是由类的构造函数创建的,若构造函数是public的,则外部可以通过构造函数随意创建对象,若想限制类对象的创建,可以将构造函数改为private,至少也要protected。但是要保证类的可用性,就需要对外提供一个方法用于访问该类。

    3、单例的优点和缺点

    优点:在内存中一个类只有一个实例,避免对象的不断创建和销毁,减少内存开销
    缺点:在编写单例代码时需要解决线程安全问题和序列化问题

    4、单例的写法和比较

    单例的写法分为饿汉式、懒汉式、静态内部类、枚举、

    ①饿汉式
    public class Singleton {
        //实例化
        private static Singleton instance = new Singleton();
        
        //私有化的构造函数
        private Singleton(){}
        
        //对外提供一个统一获取实例的静态方法
        public static Singleton getInstance(){
            return instance;
        }
    }
    

    线程安全:安全。因为instance对象是static修饰的,在类第一次被加载时,instance就已经被初始化完成,所以线程安全。
    缺点:类第一次被加载时就实例化,若实例用不到,会造成资源的浪费。若类被多次加载,会产生多个实例化对象。

    ②静态内部类
    public class Singleton {
    
        //私有化的构造函数
        private Singleton(){}
    
        //静态内部类实例化
        private static class SingletonHolder{
            private static final Singleton INSTANCE = new Singleton();
        }
    
        //对外提供统一方法
        public static Singleton getInstance(){
            return SingletonHolder.INSTANCE;
        }
    }
    

    静态内部类的方式解决了饿汉式资源浪费的问题,当Singleton类第一次被加载时,instance不一定初始化,只有显示调用getInstance()方法时,才会装载SingletonHolder类,进而instance被初始化,这种延迟加载的方式解决了饿汉式浪费资源的问题。
    线程安全:安全。与饿汉式一样用static修饰,利用classLoader机制的线程安全性保证此种单例的线程安全。

    ③懒汉式
    public class Singleton {
    
        private static Singleton instance;
    
        //私有化的构造函数
        private Singleton(){}
    
        //对外提供统一方法
        public static Singleton getInstance(){
            if (null == instance){
                instance = new Singleton();
            }
            return instance;
        }
    }
    

    懒汉式是在对象第一次被使用时初始化instance。
    线程安全:不安全。若线程A和线程B同时请求调用getInstance()方法,当A进入if判断,但是没有执行new操作时,B也进行if判断,此时inatance为null,这种情况线程A和线程B会分别new不同的Singleton对象。

    ④懒汉式改进版一
    public class Singleton {
    
        private static Singleton instance;
    
        //私有化的构造函数
        private Singleton(){}
    
        //对外提供统一方法
        public static synchronized Singleton getInstance(){
            if (null == instance){
                instance = new Singleton();
            }
            return instance;
        }
    }
    

    为了解决懒汉式的线程安全问题,用synchronized修饰方法。
    线程安全:安全。
    缺点:由于getInstance方法被整个锁住,所以多线程环境下,此方法是串行执行的,执行效率低。

    ⑤懒汉式改进版二
    public class DoubleCheckLazySingleton {
        private  static  DoubleCheckLazySingleton instance;
    
        private DoubleCheckLazySingleton(){};
    
        public static DoubleCheckLazySingleton getInstance(){
            if (instance == null){
                synchronized (DoubleCheckLazySingleton.class){
                    if (instance == null){
                        instance = new DoubleCheckLazySingleton();
                    }
                }
            }
            return instance;
        }
    }
    

    双重校验锁懒汉式单例,将synchronized锁范围缩小至初始化代码,从而提高执行效率,采用两次空值判断。
    线程安全:不安全。
    缺点:JVM可以执行重排序,指令的执行顺序:(1)分配内存给instance对象。(2)初始化instance对象。(3)设置instance对象指向分配内存地址。正常我们认为的执行顺序是(1)(2)(3),由于指令重排序机制,最后执行的顺序可能是(1)(3)(2),此时线程A执行(3),但没有执行(2),线程B进来之后判断instance对象非空(正在执行(2)或者未执行(2)),程序会产生错误。
    那么如何解决上面的问题呢?volatile解决线程间的可见性和指令重排序问题。

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

    将instance变量用volatile修饰之后可解决以上问题。
    但是它只是看上去完美无缺,其实还是有问题的。
    缺点:序列化问题(后续展开说明)

    ⑥枚举单例
    public enum EnumSingleton {
        INSTANCE;
    
        private EnumSingleton(){};
    
        public static EnumSingleton getInstance(){
            return INSTANCE;
        }
    }
    

    这种方式是Effective Java作者Josh Bloch 提倡的方式。枚举类型单例,即解决了线程安全问题,有解决了序列化问题。
    优点:

    线程安全

    枚举在底层做了线程安全的操作。

    public final class EnumSingleton extends Enum
    {
    
        public static EnumSingleton[] values()
        {
            return (EnumSingleton[])$VALUES.clone();
        }
    
        public static EnumSingleton valueOf(String name)
        {
            return (EnumSingleton)Enum.valueOf(com/xxx/EnumSingleton, name);
        }
    
        private EnumSingleton(String s, int i)
        {
            super(s, i);
        }
    
        public static EnumSingleton getInstance()
        {
            return INSTANCE;
        }
    
        public static final EnumSingleton INSTANCE;
        private static final EnumSingleton $VALUES[];
    
        static 
        {
            INSTANCE = new EnumSingleton("INSTANCE", 0);
            $VALUES = (new EnumSingleton[] {
                INSTANCE
            });
        }
    }
    

    以上代码是枚举类反编译之后的代码,我们发现变成了继承Enum类的final class类,变量instance用static修饰,instance的初始化在static块中,其实就是上面我们讲到的饿汉式单例。所以是线程安全的。

    5、序列化破坏单例:

    其他的单例模式都会有序列化问题,我们来以双重校验锁单例对象的序列化和反序列化做个测试:

    public class SerializableTest {
        public static void main(String[] args) throws IOException, ClassNotFoundException {
            ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("testFile"));
            //写入双重校验锁单例对象
            DoubleCheckLazySingleton s1 = DoubleCheckLazySingleton.getInstance();
            oos.writeObject(s1);
            oos.flush();
            oos.close();
    
            //反序列化
            ObjectInputStream ois = new ObjectInputStream(new FileInputStream("testFile"));
            DoubleCheckLazySingleton s2 = (DoubleCheckLazySingleton) ois.readObject();
            ois.close();
    
            System.out.println("s1:"+s1);
            System.out.println("s2:"+s2);
            System.out.println("s1==s2:"+ (s1==s2));
    
        }
    }
    

    输出结果:

    s1:com.zhangjq.DoubleCheckLazySingleton@14ae5a5
    s2:com.zhangjq.DoubleCheckLazySingleton@6d03e736
    s1==s2:false
    

    从结果可以看出序列化和反序列化不是同一个对象,那么是什么原因造成的呢?我们来看源码分析:
    ObjectInputStream.readObject()

        public final Object readObject()
            throws IOException, ClassNotFoundException
        {
            if (enableOverride) {
                return readObjectOverride();
            }
    
            // if nested read, passHandle contains handle of enclosing object
            int outerHandle = passHandle;
            try {
                Object obj = readObject0(false);
                handles.markDependency(outerHandle, passHandle);
                ClassNotFoundException ex = handles.lookupException(passHandle);
                if (ex != null) {
                    throw ex;
                }
                if (depth == 0) {
                    vlist.doCallbacks();
                }
                return obj;
            } finally {
                passHandle = outerHandle;
                if (closed && depth == 0) {
                    clear();
                }
            }
        }
    

    在这里调用了readObject0(),在这个方法中可以看到

    private Object readObject0(boolean unshared) throws IOException {
          ...
          case TC_OBJECT:
          return checkResolve(readOrdinaryObject(unshared));
          ...
    }
    

    我们继续进入readOrdinaryObject方法

    private Object readOrdinaryObject(boolean unshared)
            throws IOException
        {
            if (bin.readByte() != TC_OBJECT) {
                throw new InternalError();
            }
    
            ObjectStreamClass desc = readClassDesc(false);
            desc.checkDeserialize();
    
            Class<?> cl = desc.forClass();
            if (cl == String.class || cl == Class.class
                    || cl == ObjectStreamClass.class) {
                throw new InvalidClassException("invalid class descriptor");
            }
    
            Object obj;
            try {
                obj = desc.isInstantiable() ? desc.newInstance() : null;
            } catch (Exception ex) {
                throw (IOException) new InvalidClassException(
                    desc.forClass().getName(),
                    "unable to create instance").initCause(ex);
            }
            ·········
            return obj;
        }
    

    可以看到这样一段代码obj = desc.isInstantiable() ? desc.newInstance() : null;
    进入isInstantiable方法,发现就是一个判断是否存在无参构造函数的语句

    /** serialization-appropriate constructor, or null if none */
        private Constructor<?> cons;
    ·········
    /**
         * Returns true if represented class is serializable/externalizable and can
         * be instantiated by the serialization runtime--i.e., if it is
         * externalizable and defines a public no-arg constructor, or if it is
         * non-externalizable and its first non-serializable superclass defines an
         * accessible no-arg constructor.  Otherwise, returns false.
         */
    boolean isInstantiable() {
            requireInitialized();
            return (cons != null);
        }
    

    综合以上所述:我们发现只要序列化的类存在无参构造函数就调用desc.newInstance(),构造一个新的对象返回。

    解决方案:

    只需要在序列化的类中添加readResolve()方法即可解决序列化问题

    public class DoubleCheckLazySingleton implements Serializable{
        private volatile static  DoubleCheckLazySingleton instance;
    
        private DoubleCheckLazySingleton(){};
    
        public static DoubleCheckLazySingleton getInstance(){
            if (instance == null){
                synchronized (DoubleCheckLazySingleton.class){
                    if (instance == null){
                        instance = new DoubleCheckLazySingleton();
                    }
                }
            }
            return instance;
        }
    
        private Object readResolve(){
            return instance;
        }
    }
    

    再次运行序列化和反序列化操作,结果如下:

    s1:com.zhangjq.DoubleCheckLazySingleton@14ae5a5
    s2:com.zhangjq.DoubleCheckLazySingleton@14ae5a5
    s1==s2:true
    

    为什么添加readResolve()方法就可以解决序列化问题呢?我们来看源码:
    readOrdinaryObject()方法中,判断是否存在无参构造函数之后,又判断了是否存在readResolve()的方法

    if (obj != null &&
                handles.lookupException(passHandle) == null &&
                desc.hasReadResolveMethod())
            {
                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);
                        }
                    }
                    handles.setObject(passHandle, obj = rep);
                }
            }
    
    

    hasReadResolveMethod:

    /**
         * Returns true if represented class is serializable or externalizable and
         * defines a conformant readResolve method.  Otherwise, returns false.
         */
        boolean hasReadResolveMethod() {
            requireInitialized();
            return (readResolveMethod != null);
        }
    

    若存在则执行hasReadResolveMethod方法,结果覆盖上面new 出来的对象,返回。
    那么readResolveMethod 是在哪里赋值的呢?通过全局查找找到了赋值代码在私有方法
    ObjectStreamClass()方法中给 readResolveMethod 进行赋值,来看代码:

    readResolveMethod = getInheritableMethod(cl, "readResolve", null, Object.class);
    

    在 invokeReadResolve()方法中用反射调用了 readResolveMethod 方法。
    通过 JDK 源码分析我们可以看出,虽然,增加 readResolve()方法返回实例,解决了单
    例被破坏的问题。但是,我们通过分析源码以及调试,我们可以看到实际上实例化了两
    次,只不过新创建的对象没有被返回而已。那如果,创建对象的动作发生频率增大,就
    意味着内存分配开销也就随之增大。

    枚举单例序列化

    对上述测试代码做修改

    public class SerializableTest {
        public static void main(String[] args) throws IOException, ClassNotFoundException {
            ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("testFile"));
            //写入双重校验锁单例对象
    //        DoubleCheckLazySingleton s1 = DoubleCheckLazySingleton.getInstance();
            EnumSingleton s1 = EnumSingleton.getInstance();
            oos.writeObject(s1);
            oos.flush();
            oos.close();
    
            //反序列化
            ObjectInputStream ois = new ObjectInputStream(new FileInputStream("testFile"));
    //        DoubleCheckLazySingleton s2 = (DoubleCheckLazySingleton) ois.readObject();
            EnumSingleton s2 = (EnumSingleton) ois.readObject();
            ois.close();
    
            System.out.println("s1:"+s1);
            System.out.println("s2:"+s2);
            System.out.println("s1==s2:"+ (s1==s2));
    
        }
    }
    

    运行结果:

    s1:INSTANCE
    s2:INSTANCE
    s1==s2:true
    

    没什么任何附加的操作,就可以保证序列化问题,我们来看源码分析:
    继续分析ObjectInputStream.readObject(),在readObject0方法中

                    case TC_ENUM:
                        return checkResolve(readEnum(unshared));
    

    我们看到在 readObject0()中调用了 readEnum()方法,来看 readEnum()中代码实现:

        private Enum<?> readEnum(boolean unshared) throws IOException {
            if (bin.readByte() != TC_ENUM) {
                throw new InternalError();
            }
    
            ObjectStreamClass desc = readClassDesc(false);
            if (!desc.isEnum()) {
                throw new InvalidClassException("non-enum class: " + desc);
            }
    
            int enumHandle = handles.assign(unshared ? unsharedMarker : null);
            ClassNotFoundException resolveEx = desc.getResolveException();
            if (resolveEx != null) {
                handles.markException(enumHandle, resolveEx);
            }
    
            String name = readString(false);
            Enum<?> result = null;
            Class<?> cl = desc.forClass();
            if (cl != null) {
                try {
                    @SuppressWarnings("unchecked")
                    Enum<?> en = Enum.valueOf((Class)cl, name);
                    result = en;
                } catch (IllegalArgumentException ex) {
                    throw (IOException) new InvalidObjectException(
                        "enum constant " + name + " does not exist in " +
                        cl).initCause(ex);
                }
                if (!unshared) {
                    handles.setObject(enumHandle, result);
                }
            }
    
            handles.finish(enumHandle);
            passHandle = enumHandle;
            return result;
        }
    

    可以看出枚举对象反序列化时是通过Enum.valueOf()方法根据名字查找枚举对象 。不存在new新对象的情况,所以枚举单例的反序列化问题是安全的。

    6、反射破坏单例

    在上述所有的单例模式中,构造方法都是private权限的,很多人以为只要设置成private就是安全的了,其实不然,利用反射机制可以强制访问private的构造方法来创建对象。我们来看一个例子:

    public class ReflectSingletonTest {
        public static void main(String[] args) throws IllegalAccessException, InvocationTargetException, InstantiationException, NoSuchMethodException {
            //以双重校验锁单例为例
            Class<?> clazz = DoubleCheckLazySingleton.class;
            Constructor<?> declaredConstructor = clazz.getDeclaredConstructor();
    
            //设置强制访问
            declaredConstructor.setAccessible(true);
    
            //初始化
            Object o1 = declaredConstructor.newInstance();
            Object o2 = declaredConstructor.newInstance();
    
            System.out.println("o1:"+o1);
            System.out.println("o2:"+o2);
            System.out.println("o1==o2:"+ (o1==o2));
        }
    }
    

    运行结果:

      o1:com.zhangjq.DoubleCheckLazySingleton@1b6d3586
    o2:com.zhangjq.DoubleCheckLazySingleton@4554617c
    o1==o2:false
    
    解决方案:

    针对这种情况,我们可以在构造函数里面加一些判断,来解决反射破坏单例的问题:

    public class DoubleCheckLazySingleton implements Serializable{
        private volatile static  DoubleCheckLazySingleton instance;
        private volatile static boolean initialized = false;
        private DoubleCheckLazySingleton(){
            if(!initialized){
                initialized = !initialized;
            }else{
               throw new RuntimeException("不允许重复创建DoubleCheckLazySingleton对象!");
            }  
        }
    
        public static DoubleCheckLazySingleton getInstance(){
            if (instance == null){
                synchronized (DoubleCheckLazySingleton.class){
                    if (instance == null){
                        instance = new DoubleCheckLazySingleton();
                    }
                }
            }
            return instance;
        }
    
        private Object readResolve(){
            return instance;
        }
    }
    

    再次运行结果如下:

    java.lang.reflect.InvocationTargetException
        at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
        at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62)
        at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
        at java.lang.reflect.Constructor.newInstance(Constructor.java:423)
        at com.zhangjq.ReflectSingletonTest.main(ReflectSingletonTest.java:25)
    Caused by: java.lang.RuntimeException: 不允许创建多个实例
        at com.zhangjq.LazyDoubleCheckSingleton.<init>(LazyDoubleCheckSingleton.java:13)
        ... 5 more
    
    枚举单例反射问题

    那么枚举类型的单例是否存在反射问题呢?

    public class ReflectSingletonTest {
        public static void main(String[] args) throws IllegalAccessException, InvocationTargetException, InstantiationException, NoSuchMethodException {
            //以双重校验锁单例为例
            Class<?> clazz = EnumSingleton.class;
            Constructor<?> declaredConstructor = clazz.getDeclaredConstructor(null);
    
            //设置强制访问
            declaredConstructor.setAccessible(true);
    
            //初始化
            Object o1 = declaredConstructor.newInstance();
            Object o2 = declaredConstructor.newInstance();
    
            System.out.println("o1:"+o1);
            System.out.println("o2:"+o2);
            System.out.println("o1==o2:"+ (o1==o2));
      }
    }
    

    输出结果:

    Exception in thread "main" java.lang.NoSuchMethodException: com.zhangjq.EnumSingleton.<init>()
        at java.lang.Class.getConstructor0(Class.java:3082)
        at java.lang.Class.getDeclaredConstructor(Class.java:2178)
        at com.zhangjq.ReflectSingletonTest.main(ReflectSingletonTest.java:13)
    

    异常的意思是没有找到EnumSingleton无参的构造函数。
    查看Enum类的源码发现只有一个带参数的构造函数

    protected Enum(String name, int ordinal) {
            this.name = name;
            this.ordinal = ordinal;
        }
    

    我们将代码修改一下,再次运行:

    public class ReflectSingletonTest {
        public static void main(String[] args) throws IllegalAccessException, InvocationTargetException, InstantiationException, NoSuchMethodException {
            //以双重校验锁单例为例
            Class<?> clazz = EnumSingleton.class;
            Constructor<?> declaredConstructor = clazz.getDeclaredConstructor(String.class,int.class);
    
            //设置强制访问
            declaredConstructor.setAccessible(true);
    
            //初始化
            Object o1 = declaredConstructor.newInstance("zhang",10);
            Object o2 = declaredConstructor.newInstance("wang",11);
    
            System.out.println("o1:"+o1);
            System.out.println("o2:"+o2);
            System.out.println("o1==o2:"+ (o1==o2));
    
    Exception in thread "main" java.lang.IllegalArgumentException: Cannot reflectively create enum objects
        at java.lang.reflect.Constructor.newInstance(Constructor.java:417)
        at com.zhangjq.ReflectSingletonTest.main(ReflectSingletonTest.java:19)
    

    报错信息很明显:不能用反射创建枚举类对象。
    我们查看newInstance()方法的源码

        public T newInstance(Object ... initargs)
            throws InstantiationException, IllegalAccessException,
                   IllegalArgumentException, InvocationTargetException
        {
            if (!override) {
                if (!Reflection.quickCheckMemberAccess(clazz, modifiers)) {
                    Class<?> caller = Reflection.getCallerClass();
                    checkAccess(caller, clazz, null, modifiers);
                }
            }
            if ((clazz.getModifiers() & Modifier.ENUM) != 0)
                throw new IllegalArgumentException("Cannot reflectively create enum objects");
            ConstructorAccessor ca = constructorAccessor;   // read volatile
            if (ca == null) {
                ca = acquireConstructorAccessor();
            }
            @SuppressWarnings("unchecked")
            T inst = (T) ca.newInstance(initargs);
            return inst;
        }
    

    可以看出当修饰符是枚举类型时,抛出异常,也就是说jdk控制反射不能创建枚举类对象。所以枚举类的反射破坏问题是不存在的。

    7、不使用synchronized和lock,如何实现一个线程安全的单例?

    以上我们讲的所有单例的线程安全都显示或者隐示的用到了ClassLoader的线程安全机制。
    饿汉式:用static修饰,类第一次被加载时实例化,ClassLoader的类加载是synchronized修饰的。
    懒汉式:显示的用synchronized修改。
    枚举:编译器编译之后枚举中的所有变量都用static final 修饰,并且是初始化在static块中
    那么如果我们不用synchronized和lock,如何实现一个线程安全的单例呢?
    答案是:CAS

    CAS是项乐观锁技术,当多个线程尝试使用CAS同时更新同一个变量时,只有其中一个线程能更新变量的值,而其它线程都失败,失败的线程并不会被挂起,而是被告知这次竞争中失败,并可以再次尝试。
    实现单例代码:

    public class Singleton {
        private static final AtomicReference<Singleton> INSTANCE = new AtomicReference<Singleton>(); 
    
        private Singleton() {}
    
        public static Singleton getInstance() {
            for (;;) {
                Singleton singleton = INSTANCE.get();
                if (null != singleton) {
                    return singleton;
                }
    
                singleton = new Singleton();
                if (INSTANCE.compareAndSet(null, singleton)) {
                    return singleton;
                }
            }
        }
    }
    

    优点:CAS依赖底层硬件的实现,没有线程之间切换和阻塞问题。
    缺点:可能一直处于等待中,不断的循环,对CPU造成资源开销。

    8、JDK中的单例

    java.lang.Runtime

    Runtime类封装了Java运行时的环境。每一个java程序实际上都是启动了一个JVM进程,那么每个JVM进程都是对应这一个Runtime实例,此实例是由JVM为其实例化的。每个 Java 应用程序都有一个 Runtime 类实例,使应用程序能够与其运行的环境相连接。
    由于Java是单进程的,所以,在一个JVM中,Runtime的实例应该只有一个。所以应该使用单例来实现。

    public class Runtime {
        private static Runtime currentRuntime = new Runtime();
    
        public static Runtime getRuntime() {
            return currentRuntime;
        }
    
        private Runtime() {}
    }
    

    以上代码为JDK中Runtime类的部分实现,可以看到,这其实是饿汉式单例模式。在该类第一次被classloader加载的时候,这个实例就被创建出来了。

    一般不能实例化一个Runtime对象,应用程序也不能创建自己的 Runtime 类实例,但可以通过 getRuntime 方法获取当前Runtime运行时对象的引用。

    9、引用

    参考:
    单例与序列化的那些事儿
    设计模式(二)——单例模式
    不使用synchronized和lock,如何实现一个线程安全的单例?
    JDK中的那些单例

    相关文章

      网友评论

          本文标题:设计模式——单例模式

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