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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(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解析中描述。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值