单例模式详解

2022年1月14日 21点热度 0条评论 来源: amazing_yml

单例模式详解

1.1单例模式概述

单例模式(Singleton Pattern)指确保一个类在任何情况下都绝对只有一个实例,并提供一个全局访问点,属于创建型设计模式。

1.2单例模式的应用场景

单例模式可以保证JVM中只存在单一实例,应用场景主要有以下几个方面:

  • 需要频繁创建一些类的对象,使用单例模式可以降低系统的内存压力,减少GC。
  • 一些类创建实例的过程复杂且占用资源过多,或耗时较长,并且经常使用。
  • 系统上需要单一控制逻辑的操作。

1.3单例模式的写法分类

1.3.1“饿汉式”单例写法

public class HungrySingleton {
    private static final HungrySingleton instance = new HungrySingleton();

    private HungrySingleton() {
    }
    
    public static HungrySingleton getInstance(){
        return instance;
    }
}

上边是饿汉式单例的标准写法,可以看到饿汉式这种方式会在类加载的时候立刻初始化,并且创建单例对象,它是绝对的线程安全,因为在线程还没有访问的时候已经实例化结束,不可能存在访问安全的问题。

  • 饿汉式的另一种写法
public class HungrySingleton {
private static final HungrySingleton instance;

    static {
        instance = new HungrySingleton();
    }

    private HungrySingleton(){}

    public static HungrySingleton getInstance(){
        return instance;
    }
}

这种写法采用静态代码块的机制。

饿汉式单例优缺点分析

饿汉式单例的写法适用于单例对象较少的情况,这样写可以保证绝对的线程安全,执行效率比较高。但是缺点也很明显,饿汉式会在类加载的时候就将所有单例对象实例化,这样系统中如果有大量的饿汉式单例对象的存在,系统初始化的时候会造成大量的内存浪费,从而导致系统的内存不可控,换句话说就是不管对象用不用,对象都已存在,占用内存。
为了解决饿汉式写法带来内存浪费的问题,引出了懒汉式写法。

1.3.2懒汉式单例写法

  • 特点:单例对象在被使用的时候才会进行实例化
public class LazySingleton {
    private LazySingleton(){}

    private static LazySingleton instance;

    public static LazySingleton getInstance(){
        if (instance == null){
            instance = new LazySingleton();
        }
        return instance;
    }
}

上边是懒汉式单例的标准写法,从代码可以看出单例对象只有在被使用的时候才会进行实例化,但是这种方式又会引入一个新的问题。在多线程的环境下执行,存在线程安全问题,可能破坏单例。
验证一下:

public class ExcutorThread implements Runnable{
    @Override
    public void run() {
        LazySingleton instance = LazySingleton.getInstance();
        System.out.println(Thread.currentThread().getName()+":"+instance);
    }
}
public class LazySingletonTest {
    public static void main(String[] args) {
        Thread thread1 = new Thread(new ExcutorThread());
        Thread thread2 = new Thread(new ExcutorThread());

        thread1.start();
        thread2.start();
        System.out.println("end");
    }
}

由于我们这里只有两条线程模拟多线程执行,需进行多次才能获取破坏单例情况,以下为捕捉到的单例被破坏情况:

那么我们如何解决线程不安全呢?第一个应该想到的就是synchronized关键字:

public class LazySingleton {
    private LazySingleton(){}

    private static LazySingleton instance;

    public static synchronized LazySingleton getInstance(){
        if (instance == null){
            instance = new LazySingleton();
        }
        return instance;
    }
}

这样我们就解决了线程安全问题,虽然目前解决了线程安全问题,但是当调用getInstance方法的线程很多,只有一个线程RUNNING,其他线程会出现阻塞,又引入了性能问题,我们来进一步优化。

  • 双重检锁方式(Double Check)
public class LazySingleton {
    private LazySingleton(){}

    private volatile static LazySingleton instance;

    public static LazySingleton getInstance(){
        // 是否会进行阻塞
        if (instance == null){
            synchronized (LazySingleton.class){
                // 是否需要创建实例
                if (instance == null){
                    instance = new LazySingleton();
                }
            }
        }
        return instance;
    }
}

减少锁的碰撞几率,并且将锁放置到getInstance内部,调用者感受不明显。
但是只要有锁就会对性能有一定影响,这时我们从类初始化的角度考虑,引出静态内部类的方式。

  • 静态内部类写法
public class LazySingletonInnerClass {
    private LazySingletonInnerClass(){}

    public static LazySingletonInnerClass getInstance(){
        return LazyHolder.INSTANCE;
    }

    private static class LazyHolder{
        private static final LazySingletonInnerClass INSTANCE = new LazySingletonInnerClass();
    }
}

只有在调用getInstance将内部类加载,实现懒加载,内部类只加载一次,线程安全。
思考:上边这种方式就真的完美了吗?我们所有的单例模式中构造方法私有化,我们一直在获取实例的方法下功夫,而JAVA提供的反射是完全可以破坏私有化构造方法的,接下来尝试用反射破坏:

public static void main(String[] args) {
    try{
            Class<?> clazz = LazySingletonInnerClass.class;
            Constructor<?> declaredConstructor = clazz.getDeclaredConstructor(null);
            // 开启强制访问
            declaredConstructor.setAccessible(true);
            // 获取两个实例  破坏单例
            Object instance1 = declaredConstructor.newInstance();
            Object instance2 = declaredConstructor.newInstance();
        }catch (Exception e){
            e.printStackTrace();
        }
    }

这个时候构造方法被调用,破坏了单例,那我们彻底不让调用构造方法,调用构造方法时候进行抛异常,继续优化。

public class LazySingletonInnerClass {
    private LazySingletonInnerClass(){
        throw new RuntimeException("不允许调用构造方法");
    }

    public static LazySingletonInnerClass getInstance(){
        return LazyHolder.INSTANCE;
    }

    private static class LazyHolder{
        private static final LazySingletonInnerClass INSTANCE = new LazySingletonInnerClass();
    }
}

但是在构造方法中抛出异常,着实不妥,但是JAVA又又天然不允许反射调用构造方法的类---枚举,引出枚举单例模式

  • 枚举单例模式
public enum EnumSingleton {
    INSTANCE;
    public static EnumSingleton getInstance(){
        return INSTANCE;
    }
}

验证一下:

public static void main(String[] args) {
       try{
            Class<?> clazz = EnumSingleton.class;
            Constructor<?> c = clazz.getDeclaredConstructor(String.class, int.class);
            c.setAccessible(true);
            EnumSingleton instance =(EnumSingleton) c.newInstance("123", 12);
            System.out.println(instance);
        }catch (Exception e){
            e.printStackTrace();
        }
    }

运行结果:

真相大白,枚举不可用反射破坏构造方法,JDK的处理方式是最权威的,JDK枚举的特殊性,让代码实现更优雅。

至此,我们已经分析了各种创建单例对象时候出现的问题以及解决办法,但是创建完单例对象之后,有时候我们会使用序列化将对象写入磁盘,当下次使用时再从磁盘中反序列化转化为内存对象,这样也会破坏单例模式。
验证一下:

public static void main(String[] args) {
        HungrySingleton s1 = null;
        HungrySingleton s2 = HungrySingleton.getInstance();
        try{
            // 序列化
            FileOutputStream fileOutputStream = new FileOutputStream("s2.obj");
            ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream);
            objectOutputStream.writeObject(s2);
            objectOutputStream.flush();
            objectOutputStream.close();

            // 反序列化
            FileInputStream fileInputStream = new FileInputStream("s2.obj");
            ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream);
            s1 = (HungrySingleton) objectInputStream.readObject();
            objectInputStream.close();

            System.out.println(s1);
            System.out.println(s2);
            System.out.println(s1 == s2);
        }catch (Exception e){
            e.printStackTrace();
        }
    }

结果破坏了单例,见下图:

那么如何保证在序列化的情况下保证单例呢?很简单,只需要增加readResolve方法。

public class HungrySingleton implements Serializable {
  private static final HungrySingleton instance;

    static {
        instance = new HungrySingleton();
    }

    private HungrySingleton(){}

    public static HungrySingleton getInstance(){
        return instance;
    }
    private Object readResolve(){
        return instance;
    }
}


这个原因的分析,因为在反序列话过程中创建出了个新对象,所以需要看下readObject源码中是如何实现的。

private final Object readObject(Class<?> type)
        throws IOException, ClassNotFoundException
    {
        if (enableOverride) {
            return readObjectOverride();
        }

        if (! (type == Object.class || type == String.class))
            throw new AssertionError("internal error");

        // if nested read, passHandle contains handle of enclosing object
        int outerHandle = passHandle;
        try {
            Object obj = readObject0(type, 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();
            }
        }
    }
private Object readObject0(Class<?> type, boolean unshared) throws IOException {
......
            case TC_OBJECT:
                    if (type == String.class) {
                        throw new ClassCastException("Cannot cast an object to java.lang.String");
                    }
                    return checkResolve(readOrdinaryObject(unshared));  // 关键方法标记
......
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);
        }
passHandle = handles.assign(unshared ? unsharedMarker : obj);
        ClassNotFoundException resolveEx = desc.getResolveException();
        if (resolveEx != null) {
            handles.markException(passHandle, resolveEx);
        }

        if (desc.isExternalizable()) {
            readExternalData((Externalizable) obj, desc);
        } else {
            readSerialData(obj, desc);
        }

        handles.finish(passHandle);

        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);
            }
        }

        return obj;
    }
/**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);
    }

上边这个判断的意思是如果这个类是实现了serializable/externalizable,并且可以由序列化运行时实例化,则返回true,这个时候obj = desc.newInstance 就会创建一个新的对象,所以这个时候单例就被破坏了。
此时我们在往下边看,desc.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);
    }

根据注释说的意思是如果我们加上这个readResolve()方法,判断结果就是true,会进入if块,在看if中Object rep = desc.invokeReadResolve(obj);方法:

/**
     * Invokes the readResolve method of the represented serializable class and
     * returns the result.  Throws UnsupportedOperationException if this class
     * descriptor is not associated with a class, or if the class is
     * non-serializable or does not define readResolve.
     */
    Object invokeReadResolve(Object obj)
        throws IOException, UnsupportedOperationException
    {
        requireInitialized();
        if (readResolveMethod != null) {
            try {
                return readResolveMethod.invoke(obj, (Object[]) null);
            } catch (InvocationTargetException ex) {
                Throwable th = ex.getTargetException();
                if (th instanceof ObjectStreamException) {
                    throw (ObjectStreamException) th;
                } else {
                    throwMiscException(th);
                    throw new InternalError(th);  // never reached
                }
            } catch (IllegalAccessException ex) {
                // should not occur, as access checks have been suppressed
                throw new InternalError(ex);
            }
        } else {
            throw new UnsupportedOperationException();
        }
    }

这个方法可以看出,利用反射去执行我们类中的readResolve()方法,这块readResolveMethod的赋值,可以再ObjectStreamClass类中:

private ObjectStreamClass(final Class<?> cl) {
        this.cl = cl;
        name = cl.getName();
        isProxy = Proxy.isProxyClass(cl);
        isEnum = Enum.class.isAssignableFrom(cl);
        serializable = Serializable.class.isAssignableFrom(cl);
        externalizable = Externalizable.class.isAssignableFrom(cl);

        Class<?> superCl = cl.getSuperclass();
        superDesc = (superCl != null) ? lookup(superCl, false) : null;
        localDesc = this;

        if (serializable) {
            AccessController.doPrivileged(new PrivilegedAction<Void>() {
                public Void run() {
                    if (isEnum) {
                        suid = Long.valueOf(0);
                        fields = NO_FIELDS;
                        return null;
                    }
                    if (cl.isArray()) {
                        fields = NO_FIELDS;
                        return null;
                    }

                    suid = getDeclaredSUID(cl);
                    try {
                        fields = getSerialFields(cl);
                        computeFieldOffsets();
                    } catch (InvalidClassException e) {
                        serializeEx = deserializeEx =
                            new ExceptionInfo(e.classname, e.getMessage());
                        fields = NO_FIELDS;
                    }

                    if (externalizable) {
                        cons = getExternalizableConstructor(cl);
                    } else {
                        cons = getSerializableConstructor(cl);
                        writeObjectMethod = getPrivateMethod(cl, "writeObject",
                            new Class<?>[] { ObjectOutputStream.class },
                            Void.TYPE);
                        readObjectMethod = getPrivateMethod(cl, "readObject",
                            new Class<?>[] { ObjectInputStream.class },
                            Void.TYPE);
                        readObjectNoDataMethod = getPrivateMethod(
                            cl, "readObjectNoData", null, Void.TYPE);
                        hasWriteObjectData = (writeObjectMethod != null);
                    }
                    domains = getProtectionDomains(cons, cl);
                    writeReplaceMethod = getInheritableMethod(
                        cl, "writeReplace", null, Object.class);
                    readResolveMethod = getInheritableMethod(
                        cl, "readResolve", null, Object.class);  //关键方法标记
                    return null;
                }
            });
        } else {
            suid = Long.valueOf(0);
            fields = NO_FIELDS;
        }

        try {
            fieldRefl = getReflector(fields, this);
        } catch (InvalidClassException ex) {
            // field mismatches impossible when matching local fields vs. self
            throw new InternalError(ex);
        }

        if (deserializeEx == null) {
            if (isEnum) {
                deserializeEx = new ExceptionInfo(name, "enum type");
            } else if (cons == null) {
                deserializeEx = new ExceptionInfo(name, "no valid constructor");
            }
        }
        for (int i = 0; i < fields.length; i++) {
            if (fields[i].getField() == null) {
                defaultSerializeEx = new ExceptionInfo(
                    name, "unmatched serializable field(s) declared");
            }
        }
        initialized = true;
    }

这个方法已经约定方法名称readResolve,这时候执行类中的readResolve方法,直接返回已经创建的实例,所以反序列化后的结果变成了在单例类中已创建的实例对象。

1.4 单例模式的扩展

1.4.1 ioc容器的启蒙

  • 容器式单例的写法
/**
 * Created by yml
 */
public class ContainerSingleton {
    private ContainerSingleton(){}

    private static Map<String,Object> ioc = new ConcurrentHashMap<>();

    public static Object getBean(String className){
            if (!ioc.containsKey(className)){
                Object obj = null;
                try{
                    obj = Class.forName(className).newInstance();
                    ioc.put(className,obj);
                }catch (Exception e){
                    e.printStackTrace();
                }
                return obj;
            }else {
                return ioc.get(className);
            }
        }
}

这是容器式单例的写法,适用于需要大量创建单例对象的场景,便于管理,但是它是非线程安全的,其实spring中存储单例对象的容器就是一个Map。

1.4.2 ThreadLocal单例详解

  • ThreadLocal单例写法
package org.example.Singleton;

/**
 * Created by yml
 */
public class ThreadLocalSingleton {
    private ThreadLocalSingleton(){}

    private static final ThreadLocal<ThreadLocalSingleton> threadInstance =
            new ThreadLocal<ThreadLocalSingleton>(){
                @Override
                protected ThreadLocalSingleton initialValue() {
                    return new ThreadLocalSingleton();
                }
            };

    public static ThreadLocalSingleton getInstance(){
        return threadInstance.get();
    }
}

ThreadLocal的单例实现不能保证创建的单例全局唯一,但是可以保证单个线程内部是唯一的,所以是线程安全的。之前保证单例线程安全的方法是加锁,这种是以时间换空间的方式,将其他线程加锁,ThreadLocal是将对象保存到ThreadLocalMap中,以空间换时间保证线程安全。

    原文作者:amazing_yml
    原文地址: https://www.cnblogs.com/amazing-yml/p/15954966.html
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系管理员进行删除。