设计模式(2)之单例模式

2023-11-13

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-AHenjiIs-1610326440502)(https://img.shields.io/badge/link-996.icu-red.svg)]
单例模式,顾名思义就是在整个系统中,只有一个实例。spring创建的bean默认就都是单例的。该设计模式可以说是上手极简单,但可挖掘的技术深度却是又相当值得玩味的一种设计模式。

我是不太喜欢饿汉式、懒汉式等叫法的,总觉的让人更容易混淆,但是现在大家都约定俗成了,本着COC(Conversion Over Configuration)的原则,就以大家熟知的名称来介绍一下单例模式的几种实现方式吧。

一、饿汉式单例模式

确保整个系统中只有一个对象,那就得在构造器上下功夫了。要知道在java中创建对象无论是通过正常的new创建也好,还是通过反射也罢。都是需要根据构造器去创建的。既然不想让其他地方创建对象。好办!构造方法私有化!

既然构造方法私有化了,那么想要创建对象,也就是想要调用构造方法,只能在这个类里来创建了。然后写一个getInstance方法将对象传出就好了。

下面就是一个饿汉式单例的样例代码,饿汉这个叫法让人很费解。这里看一下英文原文:the singleton instance is early created at compile time。相信一下子就可以理清该方式的用途了。(个人观点,实在是不喜欢这样的翻译风格)还不如叫勤快式模式

public class EarlySingleton {

    //一加载就创建对象
    private static final EarlySingleton EARLY_SINGLETON = new EarlySingleton();

    //构造方法私有化
    private EarlySingleton(){}

    //对外提供一个公共的方法,获得该实例
    public static EarlySingleton getInstance(){
        return EARLY_SINGLETON;
    }
}

还有一种方法时利用静态代码块来实现的:

public class EarlyStaticSingleton {
    private static final EarlyStaticSingleton EARLY_STATIC_SINGLETON;
    //在静态代码块中初始化实例
    static{
        EARLY_STATIC_SINGLETON = new EarlyStaticSingleton();
    }
    //构造方法私有化
    private EarlyStaticSingleton(){}
    //对外提供实例
    public static EarlyStaticSingleton getInstance(){
        return EARLY_STATIC_SINGLETON;
    }
}

然后我们来测试一下:

public class EarlySingletonTest {
    public static void main(String[] args) {
        EarlySingleton e1 = EarlySingleton.getInstance();
        EarlySingleton e2 = EarlySingleton.getInstance();
        //由于是同一个对象,所以返回的是true
        System.out.println(e1 == e2);

        EarlyStaticSingleton es1 = EarlyStaticSingleton.getInstance();
        EarlyStaticSingleton es2 = EarlyStaticSingleton.getInstance();
        //由于是同一个对象,所以返回的是true
        System.out.println(es1 == es2);
    }
}

这里,由于一开始就已经创建了对象实例,任何后续的调用都是通过getInstance方法获取这个类一装载就已经创建好的对象。所以就不会有线程安全问题。

但是饿汉式存在一个相当不友好的问题。当创建该对象极耗时或者极耗资源,并且该对象在业务流程中不一定会用到,那么这样的损耗是相当奢侈的。接下来就讲一个被称为懒汉式单例模式的解决方案。

二、懒汉式单例模式

1、线程不安全的
既然不想让类一装载就创建对象,那就把对象的创建过程放到getInstance中呗,详见如下代码:

public class LazySingleton {
    //首先声明一个空对象
    private static LazySingleton lazySingleton = null;

    //老规矩,构造方法私有化
    private LazySingleton() {}

    //对外抛出实例
    public static LazySingleton getInstance(){
        //首先判断是否为空,如果是空,则表示是第一次调用该方法,在这个时候创建实例
        if(lazySingleton == null){
            lazySingleton = new LazySingleton();
        }
        return lazySingleton;
    }
}

然而,这是非常不安全的,在并发场景下,假设两个线程同时运行到if(lazySingleton == null){,那么由于对象还未创建,两个线程都会进入分支,新建对象,且必然是两个不同的对象。写一个测试类:

public class LazySingletonTest {
    //使用线程测试
    private static class ExecutorThread implements Runnable{
        @Override
        public void run() {
            LazySingleton lazySingleton = LazySingleton.getInstance();
            System.out.println(Thread.currentThread().getName() + " : " + lazySingleton);
        }
    }

    public static void main(String[] args) {
        Thread t1 = new Thread(new ExecutorThread());
        Thread t2 = new Thread(new ExecutorThread());
        t1.start();
        t2.start();
    }
}

输出的结果对象可能是相同的,也可能是不同的。如下是一种可能的场景:

Thread-0 : singleton.lazy.LazySingleton@1850a8cc
Thread-1 : singleton.lazy.LazySingleton@48ff0490

2、线程安全的
那么如何让线程安全呢,学过并发的都应该想到了关键字synchronized。直接在方法声明上加上该关键字,就可以保证在一条线程执行该方法的时候,其他线程则处于MONITOR状态,等待获取加锁方法或对象。
代码如下:

public class LazySecuritySingleton {
    //先声明一个空对象
    private static LazySecuritySingleton lazySecuritySingleton = null;
    //构造方法私有化
    private LazySecuritySingleton() {}
    //对外提供实例
    public synchronized static LazySecuritySingleton getInstance(){
        if(lazySecuritySingleton == null){
            lazySecuritySingleton = new LazySecuritySingleton();
        }
        return lazySecuritySingleton;
    }
}

测试代码就不上了,因为和1中的差不多,且输出结果无论如何两个对象都是同一个对象。

3、双检锁
但是直接将synchronized关键字加载方法上,每一个线程调用该方法都会产生锁竞争,极大的浪费了系统资源。为了解决这个问题,可以将synchronized关键字下沉到if方法中。这样其他线程调用该方法的时候,如果对象已经初始化,将不会产生锁竞争。

第二个if也是非常重要的。因为最初的几个线程产生竞争后,如果不判断一下,将会导致每一个线程都会初始化一次对象。

代码如下:

public class LazyDoubleSingleton {
    //volatile关键字防止指令重排序问题
    private volatile static LazyDoubleSingleton lazyDoubleSingleton = null;

    //构造方法私有化
    private LazyDoubleSingleton() {}

    //对外提供实例
    public static LazyDoubleSingleton getInstance(){
        //第一个if保证只有在刚开始的几个线程可能进入到锁竞争中
        if(lazyDoubleSingleton == null){
            synchronized (LazyDoubleSingleton.class){
                //第二个if是防止竞争锁的几个线程都创建了一遍对象
                if(lazyDoubleSingleton == null){
                    lazyDoubleSingleton = new LazyDoubleSingleton();
                }
            }
        }
        return lazyDoubleSingleton;
    }
}

这里也就不上测试代码了。

4、利用静态内部类,巧妙的避开加锁并且实现懒汉式单例
就是利用了jvm的类加载机制,完美的解决了锁竞争资源浪费和饿汉式造成的内存浪费问题:

public class LazyOuterSingleton {
    //仍然是构造方法私有化
    private LazyOuterSingleton() {}
    //对外提供实例
    public static final LazyOuterSingleton getInstance(){
        //在返回结果之前,一定会先加载内部类
        return LazyInnerSingleton.LAZY_OUTER_SINGLETON;
    }
    //加载外部类的时候,该内部类并不会被加载,而是在getInstance方法中调用该类属性的时候才会装载
    private static class LazyInnerSingleton{
        private static final LazyOuterSingleton LAZY_OUTER_SINGLETON = new LazyOuterSingleton();
    }
}

三、上述的单例模式真的能保证单例吗?一些破坏单例的手段及规避方法

虽然构造方法私有化可以确保其他类无法通过构造器初始化对象,进而达到了确保系统中至于一个该实例的目的。但是创建对象并不都是通过构造器来创建的,还有其他的创建方式。

1、利用反射破坏单例

通过反射获取构造方法,并且放开权限后初始化实例就会产生两个不同的实例。代码如下:

public class DestroySingletonByReflection {
    //构造方法私有化
    private DestroySingletonByReflection() {}
    //提供实例
    public static DestroySingletonByReflection getInstance(){
        return SingletonDemo.SINGLETON;
    }
	//利用内部类懒汉式单例测试反射破坏单例
    private static class SingletonDemo{
        private static final DestroySingletonByReflection SINGLETON = new DestroySingletonByReflection();
    }
    //测试
    public static void main(String[] args) {
        try {
            //正常获得单例实例
            DestroySingletonByReflection d1 = DestroySingletonByReflection.getInstance();
            //通过反射来获得单例
            Class<DestroySingletonByReflection> clazz = DestroySingletonByReflection.class;
            Constructor<DestroySingletonByReflection> constructor = clazz.getDeclaredConstructor();
            constructor.setAccessible(true);
            DestroySingletonByReflection d2 = constructor.newInstance();
            //输出false,显然是两个不同的对象
            System.out.println(d1 == d2);
        }catch (Exception e){
            e.printStackTrace();
        }
    }
}

可以看出反射得到的实例与正常获取的实例并不一样,这显然破坏了单例模式。为了解决这个问题,我们在构造方法中使用一些技巧来遏制反射,代码码如下:

private DestroySingletonByReflection() {
   if(SINGLETON != null){
        throw new RuntimeException("该实例只允许存在一个,请勿重新实例化!");
    }
}

加上限制后,再执行将会得到类似如下的异常提示:

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 singleton.destroy.DestroySingletonByReflection.main(DestroySingletonByReflection.java:33)
Caused by: java.lang.RuntimeException: 该实例只允许存在一个,请勿重新实例化!
	at singleton.destroy.DestroySingletonByReflection.<init>(DestroySingletonByReflection.java:17)
	... 5 more

2、利用序列化及反序列化破坏单例
序列化及反序列化的过程涉及到内存的重写,所以通过反序列化得到的对象肯定是与原对象不一致的。先将单例模式实现序列化:

public class DestroySingletonBySerializable implements Serializable {
    //利用饿汉式单例测试序列化破坏单例
    private static final DestroySingletonBySerializable SINGLETON = new DestroySingletonBySerializable();
    //构造方法私有化
    private DestroySingletonBySerializable() {}
    //对外提供实例
    public static DestroySingletonBySerializable getInstance(){
        return SINGLETON;
    }
}

测试代码如下:

public class DestroySingletonBySerializableTest {
    public static void main(String[] args) {
        try{
            //通过常规方法获得单例
            DestroySingletonBySerializable d1 = DestroySingletonBySerializable.getInstance();
            //将已经存在的单例对象进行一次序列化及反序列化
            DestroySingletonBySerializable d2;
            //写出
            FileOutputStream fos = new FileOutputStream("DestroySingletonBySerializable.obj");
            ObjectOutputStream oos = new ObjectOutputStream(fos);
            oos.writeObject(d1);
            oos.flush();
            oos.close();
            //读入
            FileInputStream fis = new FileInputStream("DestroySingletonBySerializable.obj");
            ObjectInputStream ois = new ObjectInputStream(fis);
            d2 = (DestroySingletonBySerializable)ois.readObject();
            ois.close();
            //打印false,显然破坏了单例模式
            System.out.println(d1 == d2);
        }catch (Exception e){
            e.printStackTrace();
        }
    }
}

那么如何解决这个问题呢?
说来也非常容易,只需要在实例化的单例类中加入如下代码即可:

//确保序列化中的单例
private Object readResolve(){
    return SINGLETON;
}

在DestroySingletonBySerializable类中添加如上代码,再次运行测试,将会打印true。只是添加了一个完全不知道是什么的方法,就解决了反序列化破坏单例的问题,这是为什么呢?

这个就需要阅读java.io.ObjectInputStream#readObject源码了。该方法又调用java.io.ObjectInputStream#readObject0方法,该方法源码摘录如下:

/**
 * Underlying readObject implementation.
 */
private Object readObject0(boolean unshared) throws IOException {
    boolean oldMode = bin.getBlockDataMode();
    if (oldMode) {
        int remain = bin.currentBlockRemaining();
        if (remain > 0) {
            throw new OptionalDataException(remain);
        } else if (defaultDataEnd) {
            /*
             * Fix for 4360508: stream is currently at the end of a field
             * value block written via default serialization; since there
             * is no terminating TC_ENDBLOCKDATA tag, simulate
             * end-of-custom-data behavior explicitly.
             */
            throw new OptionalDataException(true);
        }
        bin.setBlockDataMode(false);
    }

	//获取到读入的类型为TC_OBJECT
    byte tc;
    while ((tc = bin.peekByte()) == TC_RESET) {
        bin.readByte();
        handleReset();
    }

    depth++;
    totalObjectRefs++;
    try {
        switch (tc) {
            case TC_NULL:
                return readNull();

            case TC_REFERENCE:
                return readHandle(unshared);

            case TC_CLASS:
                return readClass(unshared);

            case TC_CLASSDESC:
            case TC_PROXYCLASSDESC:
                return readClassDesc(unshared);

            case TC_STRING:
            case TC_LONGSTRING:
                return checkResolve(readString(unshared));

            case TC_ARRAY:
                return checkResolve(readArray(unshared));

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

			//进入该分支
            case TC_OBJECT:
                return checkResolve(readOrdinaryObject(unshared));

            case TC_EXCEPTION:
                IOException ex = readFatalException();
                throw new WriteAbortedException("writing aborted", ex);

            case TC_BLOCKDATA:
            case TC_BLOCKDATALONG:
                if (oldMode) {
                    bin.setBlockDataMode(true);
                    bin.peek();             // force header read
                    throw new OptionalDataException(
                        bin.currentBlockRemaining());
                } else {
                    throw new StreamCorruptedException(
                        "unexpected block data");
                }

            case TC_ENDBLOCKDATA:
                if (oldMode) {
                    throw new OptionalDataException(true);
                } else {
                    throw new StreamCorruptedException(
                        "unexpected end of block data");
                }

            default:
                throw new StreamCorruptedException(
                    String.format("invalid type code: %02X", tc));
        }
    } finally {
        depth--;
        bin.setBlockDataMode(oldMode);
    }
}

可以看到在swatch分支中,又调用了java.io.ObjectInputStream#readOrdinaryObject方法,源码如下:

/**
 * Reads and returns "ordinary" (i.e., not a String, Class,
 * ObjectStreamClass, array, or enum constant) object, or null if object's
 * class is unresolvable (in which case a ClassNotFoundException will be
 * associated with object's handle).  Sets passHandle to object's assigned
 * handle.
 */
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 &&
        //重点来了,在这判断是否有readResolve方法,如果有,则通过代理拿到刚方法的返回值
        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;
}

通过上面几个方法的源码可以分析出,反序列化的时候对是否有readResolve方法进行了判断,如果有则读到的对象就是刚方法返回的对象。而在最初的单例类中,刚方法返回的显然是同一个对象。也就是说jdk提供了这么一个方法来帮助我们防止序列化破坏单例。
细心的小伙伴应该会发现一个问题,就是不管有没有readResolve方法,readObject总会先创建一个对象,虽然这个对象在有readResolve方法的时候被抛弃不用,在未来的某个时刻会被GC回收。但是如果业务代码中出现大量的反序列化,将会导致内存浪费。那么就来看一下,单例模式的最终解决方案吧:注册式单例。

四、单例模式的最强解决方案:登记式(注册式)单例
1、利用枚举
首先我们利用枚举写一个对象,并对外提供该实例,该枚举可以存储一个对象:

public enum EnumSingleton {
    //实例
    INSTANCE;

    private Object data;
    public Object getData(){
        return data;
    }
    public void setData(Object data){
        this.data = data;
    }
    //对外公共接口
    public static EnumSingleton getInstance(){
        return INSTANCE;
    }
}

我们先尝试使用反射来破坏枚举中的单例,测试代码如下:

public class EnumSingletonReflectTest {
    public static void main(String[] args) {
        try{
            //正常的方式获取单例
            EnumSingleton e1 = EnumSingleton.getInstance();
            //利用反射尝试获得单例实例
            EnumSingleton e2;

            Class<EnumSingleton> clazz = EnumSingleton.class;
            e2 = clazz.getDeclaredConstructor().newInstance();

            System.out.println(e1 == e2);
        }catch (Exception e){
            e.printStackTrace();
        }
    }
}

运行上述代码,将会得到如下的异常信息:

java.lang.NoSuchMethodException: singleton.register.EnumSingleton.<init>()
	at java.lang.Class.getConstructor0(Class.java:3082)
	at java.lang.Class.getDeclaredConstructor(Class.java:2178)
	at singleton.register.EnumSingletonReflectTest.main(EnumSingletonReflectTest.java:18)

找不到构造方法,奇怪,在EnumSingleton类中,我们并没有写构造方法,而按照java的语法,没写构造方法,就会默认有一个空参构造。我们利用jad来反编译一下EnumSingleton的字节码文件EnumSingleton.class。得到如下代码:

public final class EnumSingleton extends Enum
{

    public static EnumSingleton[] values()
    {
        return (EnumSingleton[])$VALUES.clone();
    }

    public static EnumSingleton valueOf(String name)
    {
        return (EnumSingleton)Enum.valueOf(singleton/register/EnumSingleton, name);
    }

    private EnumSingleton(String s, int i)
    {
        super(s, i);
    }

    public Object getData()
    {
        return data;
    }

    public void setData(Object data)
    {
        this.data = data;
    }

    public static EnumSingleton getInstance()
    {
        return INSTANCE;
    }

    public static final EnumSingleton INSTANCE;
    private Object data;
    private static final EnumSingleton $VALUES[];

    static 
    {
        INSTANCE = new EnumSingleton("INSTANCE", 0);
        $VALUES = (new EnumSingleton[] {
            INSTANCE
        });
    }
}

本段代码由jad自动生成,格式有些出入,但不影响阅读。我们可以看到,编译器帮我们自动生成了一个构造方法:

private EnumSingleton(String s, int i)
    {
        super(s, i);
    }

那我们稍微修改测试代码,拿到你这个构造方法再试试:

public class EnumSingletonReflectTest {
    public static void main(String[] args) {
        try{
            //正常的方式获取单例
            EnumSingleton e1 = EnumSingleton.getInstance();
            //利用反射尝试获得单例实例
            EnumSingleton e2;

            Class<EnumSingleton> clazz = EnumSingleton.class;
            Constructor<EnumSingleton> constructor = clazz.getDeclaredConstructor(String.class, int.class);
            constructor.setAccessible(true);
            e2 = constructor.newInstance("INSTANCE", 11);

            System.out.println(e1 == e2);
        }catch (Exception e){
            e.printStackTrace();
        }
    }
}

再次运行,还是得到异常,不过异常信息不同了,如下所示:

java.lang.IllegalArgumentException: Cannot reflectively create enum objects
	at java.lang.reflect.Constructor.newInstance(Constructor.java:417)
	at singleton.register.EnumSingletonReflectTest.main(EnumSingletonReflectTest.java:22)

错误信息非常明确,不能创建枚举对象。我查阅一下java.lang.reflect.Constructor#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;
}

可以看到,在newInstance中已经明确的支出,如果是枚举类型的不允许创建枚举对象。也就是说jdk在反射的底层已经帮我们规避了反射破坏单例的可能性。

接下来我们用序列化及反序列化测试一下。
直接上代码:

public class EnumSingletonTest {
    public static void main(String[] args) {
        try{
            //通过常规方法获得单例
            EnumSingleton e1 = EnumSingleton.getInstance();
            //将已经存在的单例对象进行一次序列化及反序列化
            EnumSingleton e2;
            //写出
            FileOutputStream fos = new FileOutputStream("EnumSingleton.obj");
            ObjectOutputStream oos = new ObjectOutputStream(fos);
            oos.writeObject(e1);
            oos.flush();
            oos.close();
            //读入
            FileInputStream fis = new FileInputStream("EnumSingleton.obj");
            ObjectInputStream ois = new ObjectInputStream(fis);
            e2 = (EnumSingleton)ois.readObject();
            ois.close();
            //打印true,好神奇,我们什么操作都没做
            System.out.println(e1.getData() == e2.getData());
        }catch (Exception e){
            e.printStackTrace();
        }
    }
}

结果竟然是true,我们并没有写readResolve方法啊。还是得看jad生成的代码。我们发现在最下面有一段静态代码块,在静态代码块中以及初始化了实例。也就是说通过枚举来创建单例属于饿汉式单例模式。虽然是饿汉式单例模式,那序列化和反序列化就能不破坏其单例了吗?还是得回到java.io.ObjectInputStream#readObject0方法中,同样是那个switch判断,因为读入的是枚举类的,将会进入如下分支:

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

再进入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")
            //重点在这里,通过类名和 Class 对象类找到一个唯一的枚举对象
            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;
}

枚举类型其实通过类名和 Class 对象类找到一个唯一的枚举对象。枚举对象不可能被类加载器重复加载多次。因此也就保证了单例。

2、利用类似容器的map来创建单例
代码如下:

public class RegisterSingleton {
    //这里不考虑扩展性
    private RegisterSingleton() {}
    //建一个容器,存放实例
    private static Map<String, Object> ioc = new ConcurrentHashMap<>();
    //获得实例,如果已经存在直接获得,如果没有则先创建,并放到容器中
    public static Object getBean(String name){
        synchronized (ioc){
            if(ioc.containsKey(name)){
                return ioc.get(name);
            }else{
                Object obj = null;
                try{
                    Constructor c = Class.forName(name).getDeclaredConstructor();
                    c.setAccessible(true);
                    obj = c.newInstance();
                    ioc.put(name, obj);
                }catch (Exception e){
                    e.printStackTrace();
                }
                return obj;
            }
        }
    }
}

运用容器创建单例是为了方便在需要大量创建单例的时候的一个解决方案。创建的实例仍然需要运用到之前讲解的知识。关于容器最典型的当属spring中的ioc容器了。这里就不展开讲,我会在后续spring源码阅读中详细分析。这里只讨论单例模式。

3、线程内唯一的单例
有的时候业务需要我们在一条线程中保证单例,但是并不强求全局单例。那该如何解决呢?这里可以利用ThreadLocal来实现。详细代码如下:

public class ThreadLocalSingleton {
    private static final ThreadLocal<ThreadLocalSingleton> threadLocalInstance = ThreadLocal.withInitial(() -> new ThreadLocalSingleton());

    private ThreadLocalSingleton() {}

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

    private static class ExecutorThread implements Runnable {
        @Override
        public void run() {
            ThreadLocalSingleton threadLocalSingleton = ThreadLocalSingleton.getInstance();
            System.out.println(Thread.currentThread().getName() + " : " + threadLocalSingleton);
        }
    }

    public static void main(String[] args) {
        System.out.println(Thread.currentThread().getName() + " : " + ThreadLocalSingleton.getInstance());
        System.out.println(Thread.currentThread().getName() + " : " + ThreadLocalSingleton.getInstance());

        Thread t1 = new Thread(new ExecutorThread());
        Thread t2 = new Thread(new ExecutorThread());
        t1.start();
        t2.start();
        System.out.println("End");
    }
}

运行该代码得到的结果如下:

main : singleton.thread.ThreadLocalSingleton@4f3f5b24
main : singleton.thread.ThreadLocalSingleton@4f3f5b24
End
Thread-0 : singleton.thread.ThreadLocalSingleton@67c716ea
Thread-1 : singleton.thread.ThreadLocalSingleton@3b94507a

可以看到在不同的线程中,得到的实例是不一致的,而在相同的main线程上,对象都是一样的。

小结
单例模式可以说是非常简单的一种设计模式,只需要将构造方法私有化,并对外提供一个访问接口就可以实现;但是单例模式又是一个可以挖掘技术深度非常深的一个模式,因为要考虑到内存的使用、线程的安全、反射及反序列化对其的破坏等等问题。关于单例模式的使用,还是需要参考spring中的应用,这个我将在spring解析中描述。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

设计模式(2)之单例模式 的相关文章

  • 设计模式三: 代理模式(Proxy) -- JDK的实现方式

    简介 代理模式属于行为型模式的一种 控制对其他对象的访问 起到中介作用 代理模式核心角色 真实角色 代理角色 按实现方式不同分为静态代理和动态代理两种 意图 控制对其它对象的访问 类图 实现 JDK自带了Proxy的实现 下面我们先使用JD
  • python语法(高阶)-设计模式(单例模式)

    参考内容 黑马程序员
  • C++ 装饰器模式

    什么是装饰器模式 装饰器模式是一种结构型设计模式 实现了在不改变现有对象结构的的同时又拓展了新的功能 装饰器本质上是对现有对象的重新包装 同时装饰器又称为封装器 如何理解装饰器模式 以笔记本电脑为例 当我们购买了一台新笔记本电脑 但我们发现
  • Java设计模式:装饰者模式(Decorator Pattern)

    装饰者模式 涉及的重要设计原则 类应该对扩展开放 对修改关闭 装饰者模式定义 装饰者模式动态地将责任附加到对象上 若要扩展功能 装饰者提供了比继承更有弹性的替代方案 UML类图 装饰者模式事例 咖啡店 咖啡种类 1 深焙咖啡 DarkRoa
  • 面向过程和面向对象的语言有哪些,以及优缺点(一篇文章让你理解)

    C语言是面向过程的 而C python java是面向对象的 面向过程的编程思想将一个功能分解为一 个一个小的步骤 我们通过完成一个一 个的小的步骤来完成一个程序 优点 这种编程方式 符合我们人类的思维 编写起来相对比较简单 缺点 但是这种
  • java需会(转载)

    一 基础篇 1 1 Java基础 面向对象的特征 继承 封装和多态 final finally finalize 的区别 Exception Error 运行时异常与一般异常有何异同 请写出5种常见到的runtime exception i
  • 设计模式--组合模式

    组合模式 又叫部分整体模式 属于结构型模式 基本原理 以树形的结构将相似的对象组合起来 主要流程 1 创建对象 2 在对象中设置用来存放下一级相似对象的数据结构 3 在对象中设置增删改查等功能 注意 这种模式和数据结构中的树形结构相似 in
  • 计算资源合并模式——云计算架构常用设计模式

    背景 云计算的解决方案中 最初设计可能有意遵循关注点分离的设计原则 把操作分解为独立的计算单元以便可以单独托管和部署 然而 虽然这种策略可以帮助简化解决方案的逻辑实现 但是在同一个应用程序中要部署大量的计算单元 这会增加运行时的托管成本 并
  • C++设计模式-State状态模式

    State状态模式作用 当一个对象的内在状态改变时允许改变其行为 这个对象看起来像是改变了其类 UML图如下 State类 抽象状态类 定义一个接口以封装与Context的一个特定状态相关的行为 ConcreteState类 具体状态 每一
  • 行为型模式-状态模式

    package per mjn pattern state after 环境角色类 public class Context 定义对应状态对象的常量 public final static OpeningState OPENING STAT
  • 设计模式——简单工厂模式

    简单工厂模式定义为 简单工厂模式又称为静态工厂方法模型 它属于类创建型模式 在简单工厂模式中 可以根据参数的不同返回不同类的实例 简单工厂专门定义一个类来负责创建其他类的实例 被创建的实例通常都具有共同的父类 简单工厂模式结构图 简单工厂模
  • 设计模式(十)装饰器模式

    装饰器模式是一种非常有用的结构型模式 它允许我们在不改变类的结果的情况下 为类添加新的功能 我们来举例说明一下 首先添加一组形状 它们都实现了形状接口 public interface Shape String getShape class
  • 设计模式--提供者模式provider

    设计模式 C 提供者模式 Provider Pattern 介绍 为一个API进行定义和实现的分离 示例 有一个Message实体类 对它的操作有Insert 和Get 方法 持久化数据在SqlServer数据库中或Xml文件里 根据配置文
  • [C++]外观模式

    外观模式 Facade Pattern 隐藏系统的复杂性 并向客户端提供了一个客户端可以访问系统的接口 这种类型的设计模式属于结构型模式 它向现有的系统添加一个接口 来隐藏系统的复杂性 这种模式涉及到一个单一的类 该类提供了客户端请求的简化
  • 组合型模式

    概述 对于这个图片肯定会非常熟悉 上图我们可以看做是一个文件系统 对于这样的结构我们称之为树形结构 在树形结构中可以通过调用某个方法来遍历整个树 当我们找到某个叶子节点后 就可以对叶子节点进行相关的操作 可以将这颗树理解成一个大的容器 容器
  • 泛型与反射机制在JDBC和Servlet编程中的实践

    写在前面 泛型与反射是java中的两种强大机制 可以很好的提高代码的灵活性和复用性 本篇文章向大家展现在JDBC和Servlet编程场景下反射和泛型技术的实践 通过灵活使用这两种机制打造 高度可复用的JDBC和Servlet代码 1 JDB
  • 设计模式详解---策略模式

    1 策略模式简介 策略模式 Strategy Pattern 是一种行为型设计模式 用于在运行时根据不同的情境选择不同的算法或策略 该模式将算法封装成独立的类 使得它们可以相互替换 而且可以独立于客户端使用它们的方式 1 1 主要角色 上下
  • 设计模式—迭代器模式解析

    本文参考学习了 图解设计模式 中的代码实现和原理解释 迭代器模式 简介 Iterator 模式用于在数据集合中按照顺序遍历集合 就类似于我们的循环 一个个去遍历一个集合中的所有元素 示例代码 首先我们思考一个书本和书架的关系 显然 书架可以
  • 谁能想到Java多线程设计模式竟然能被图解,大佬就是大佬,太牛了

    设计模式 Design pattern 代表了最佳的实践 通常被有经验的面向对象的软件开发人员所采用 设计模式是软件开发人员在软件开发过程中面临的一般问题的解决方案 这些解决方案是众多软件开发人员经过相当长的一段时间的试验和错误总结出来的
  • C++设计模式 #3策略模式(Strategy Method)

    动机 在软件构建过程中 某些对象使用的的算法可能多种多样 经常改变 如果将这些算法都写在类中 会使得类变得异常复杂 而且有时候支持不频繁使用的算法也是性能负担 如何在运行时根据需求透明地更改对象的算法 将算法和对象本身解耦 从而避免上述问题

随机推荐

  • 高云FPGA系列教程(基于GW1NSR-4C TangNano 4K开发板)

    文章目录 TOC 已完成 待完成 已完成 国产FPGA高云GW1NSR 4C 集成ARM Cortex M3硬核 高云FPGA系列教程 1 FPGA和ARM开发环境搭建 高云FPGA系列教程 2 FPGA点灯工程创建 程序下载和固化 高云F
  • React面试题汇总

    1 面试官 说说对 React 的理解 有哪些特性 React遵循组件设计模式 使用虚拟 DOM 来有效地操作 DOM 遵循从高阶组件到低阶组件的单向数据流 React 特性有很多 如 JSX 语法 单向数据绑定 虚拟 DOM 声明式编程
  • 【深度学习】 - 作业7: 图像超分辨率重建

    课程链接 清华大学驭风计划 代码仓库 Victor94 king MachineLearning MachineLearning basic introduction github com 驭风计划是由清华大学老师教授的 其分为四门课 包括
  • web端上传图片时 图片被旋转问题

    有些时候在web端上传图片会遇到这种情况 正向的图片 上传预览时就被旋转了 发生这种情况是因为 照片中包含很多属性来记录拍摄信息 想要读取这些属性 需要引入EXIF 可在npm上搜索exif js下载 EXIF中 包含一个Orientati
  • Qt内存管理及泄露后定位到内存泄漏位置的方法

    Qt内存管理机制 Qt使用对象父子关系进行内存管理 在创建类的对象时 为对象指定父对象指针 当父对象在某一时刻被销毁释放时 父对象会先遍历其所有的子对象 并逐个将子对象销毁释放 Qt内存管理代码示例 QLabel label new QLa
  • Linux 查找文件(find命令/locate命令)

    目录 一 find 我的东西在哪 二 更快速地定位文件 locate命令 一 find 我的东西在哪 随着文件增多 我们有时候记住某个文件放在哪个文件夹下了 此时搜索工具显得非常有用了 而find就是这样一个命令 可以帮助我们在指定范围内查
  • 多对一的4种查询方式

    多对一的概念在数据库中是十分常见的 下面将以多个学生对应一个老师的例子介绍4种多对一的查询方式 一 建立数据库 首先建立2种表 一种是teacher表 其中包含的字段有id 主键 name 一种是student表 其中包含的字段有id 主键
  • rsa加密

    public static class RSAHelper private static string privateKey private static string publicKey public static string GetP
  • LLMs的自动化工具系统(HuggingGPT、AutoGPT、WebGPT、WebCPM)

    在前面两篇博文中已经粗略介绍了增强语言模型和Tool Learning 本篇文章看四篇代表性的自动化框架 HuggingGPT AutoGPT WebGPT WebCPM Augmented Language Models 增强语言模型 T
  • log4j Layout简介说明

    转自 log4j Layout简介说明 下文笔者讲述log4j的简介说明 如下所示 log4j Layout的功能 log4j Layout主要用于日志数据格式化 它有以下三种形式 HTMLLayout 将日志格式化为HTML表格形式 ht
  • 记录uni-app开发原生android插件,调用不了,没有返回值的问题。返回值为{}的问题。返回值为空的问题

    1 引入了原生插件但是调用不了没有返回值 这种情况大多数是开发原生插件的时候引入了aar库 但是打包的时候没有引入 把需要引入的库放在生成的文件目录下就可以比如 开发了一个叫t1 module 的插件 引入了一个printer lib 3
  • vivado时序分析 实例

    vivado时序分析实例 建立余量 保持余量 实例分析 建立余量 保持余量 实例分析 环境 Vivado 2019 2 芯片型号 xc7z020clg484 2 举例子说明怎么使用Reporte Timing Summary 建立源工程 m
  • locust 性能测试工具(V2.8.6)

    locust 性能测试工具 特点 安装 验证 Demo 编写 locustfile 配置 分布式生成负载 在调试器中运行测试 在 Docker 中运行 使用 Terraform AWS 运行分布式负载测试 不使用 web UI 运行 自定义
  • c语言程序延时参数500,C语言精确延时设计

    我现在就用两种方法来实现 一种是while 语句 另一种是for 语句 这两种语句均可产生汇编语句中的DJNZ语句 以12MHZ晶振为例 说明 在编写C程序时 变量尽量使用unsigned char 如满足不了才使用unsigned int
  • Spark学习笔记:OutOfMemoryError-Direct buffer memory (OOM)

    之前也遇到过几次关于OOM 堆外内存溢出 的问题 但都只是大体上看了看 没有细致的总结 目前了解的还不是特别清楚 只好总结一下我觉得可行的处理方案 另外贴一些原理 首先是当时的一些处理方案 第一次OOM 第一次遇到这个问题时 上网查 发现很
  • 磁盘格式化了怎么恢复里面文件

    磁盘格式化了怎么恢复里面文件 磁盘格式化了数据能恢复吗 电脑磁盘是我们生活中经常打交道的一种存储介质 我们的电脑每天在工作和学习中都要读写大量的数据 因此我们经常要清理电脑磁盘保证电脑的运行速度和内存充足 那么如果电脑磁盘被格式化了该怎么恢
  • yocto编译linux社区5.10版本的坎坷

    作为菜鲲的我 基于meta intel的bsp进行修改 精简后的linux intel 5 10 bb内容如下 require recipes kernel linux linux yocto inc FILESEXTRAPATHS pre
  • add_subdirectory(子文件夹名)用法

    add subdirectory 子文件夹名 表示对子文件夹项目进行cmake编译
  • redis命令之哈希表类型hdel命令用法详情

    哈希表 HDEL命令 命令 hdel key field field field 同时删除N个field 对于不存在的field会被忽略 并返回被删除的field的个数 当在该key下的最后一个field也被删除掉的话 再通过hget ke
  • 设计模式(2)之单例模式

    外链图片转存失败 源站可能有防盗链机制 建议将图片保存下来直接上传 img AHenjiIs 1610326440502 https img shields io badge link 996 icu red svg 单例模式 顾名思义就是