vlambda博客
学习文章列表

设计模式系列-你真的了解单例模式吗??

单例模式(Singleton Pattern)的定义相信大家都清楚,就是确保一个类在任何情况下都绝对只有一个实例,并提供一个全局访问点。接下来我们看看常见的单例模式及其优缺点。

饿汉式单例模式

public class HungrySingleton {

    private static final HungrySingleton instance = new HungrySingleton();

    private HungrySingleton(){}

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

饿汉式单例模式在类加载时就初始化,并且创建实例对象,它是线程安全的。饿汉式单例模式适用单例对象较少的系统,该写法可以保证线程安全、执行效率高。但是缺点也很明显,如果系统中存在大量单例对象,那么在系统初始化过程就创建了大量的对象实例,不管是否用到了对象实例都占用着空间,浪费内存。那么有没有更优的写法呢?

懒汉式单例模式

public class LazySimpleSingleton {

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

懒汉式单例模式解决了饿汉式单例模式类加载就创建实例的问题,只在使用时才创建实例对象。但这样写又带来一个新的问题,在多线程环境下存在线程安全问题。那么我们怎么解决懒汉式单例模式的线程安全问题呢?对多线程有一定了解的你们可能想到了,在getInstance方法上加锁即synchronized关键字,使其变为同步方法。

public class LazySimpleSingleton {

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

这种写法确实解决了内存浪费和线程安全问题,但是当大量线程同时获取实例时,只有一个线程获得锁进入getInstance方法,其他线程都要进入阻塞状态等待获得锁,导致程序性能大幅下降。那么有没有一种更好的方式既能解决线程安全问题又能兼顾性能呢?双重校验锁它来了。

public class LazyDoubleCheckSingleton {

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

双重校验锁解决了性能问题,但是终归synchronized关键字上要加锁,对于程序性能还是有一定的影响,那有没有更好的方法呢?我们可以利用类加载机制在使用时才加载实例,还可以避开线程安全问题。我们看一下代码:

public class LazyInnerSingleton {
    private LazyInnerSingleton(){}
    //final防止被重写、重载
    public static final LazyInnerSingleton getInstance() {
        return InstanceHolder.INSTANCE;
    }
    //静态内部类默认不加载,调用之前才会初始化,利用类加载机制巧妙避开了线程安全问题
    private static class InstanceHolder {
        private static final LazyInnerSingleton INSTANCE = new LazyInnerSingleton();
    }
}

反射破坏单例

上面的写法就万无一失了吗?先看一段儿代码。

public class ReflectTest {
    public static void main(String[] args) {
        try {
            Object instance1 = LazyInnerSingleton.getInstance();
            
            Class<?> clazz = LazyInnerSingleton.class;
            Constructor constructor = clazz.getDeclaredConstructor(null);
            constructor.setAccessible(true); //强制访问
            Object instance2 = constructor.newInstance();
            
            //运行结果为:false
            System.out.println(instance1 == instance2);
        }catch (Exception e) {
            e.printStackTrace();
        }
    }
}

我们可以看到,通过反射已经破坏了单例模式,那么我们怎么解决这个问题呢?是不是在私有构造方法里做一些限制,如果多次创建的话抛出异常就可以了。我们对LazyInnerSingleton类再做一下改动。

public class LazyInnerSingleton {
    private LazyInnerSingleton(){
        if(InstanceHolder.INSTANCE != null) {
            throw new RuntimeException("不允许重复创建实例");
        }
    }
    //final防止被重写、重载
    public static final LazyInnerSingleton getInstance() {
        return InstanceHolder.INSTANCE;
    }
    //静态内部类默认不加载,调用之前才会初始化,利用类加载机制巧妙避开了线程安全问题
    private static class InstanceHolder {
        private static final LazyInnerSingleton INSTANCE = new LazyInnerSingleton();
    }
}

序列化破坏单例

一个java对象创建好后,有时候需要将对象序列化后写入磁盘,使用时再从磁盘读取数据进行反序列化创建对象实例,反序列化创建的对象会重新分配内存空间,也就是会新建一个对象实例。如果一个支持序列化的对象是单例模式,很明显就违背了单例模式的初衷,我们先来看一段序列化破坏单例的代码。

public class SerializableSingleton implements Serializable {
    private static final SerializableSingleton instance = new SerializableSingleton();
    private SerializableSingleton(){}
    public static SerializableSingleton getInstance() {
        return instance;
    }
}
public class SerializableTest {
    public static void main(String[] args) {
        try {
            SerializableSingleton instance = SerializableSingleton.getInstance();
            
            ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("serializable.obj"));
            objectOutputStream.writeObject(instance);
            objectOutputStream.flush();
            objectOutputStream.close();
            
            ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream("serializable.obj"));
            SerializableSingleton serializableInstance = (SerializableSingleton) objectInputStream.readObject();
            objectInputStream.close();
            System.out.println(instance == serializableInstance);//结果:false
        }catch (Exception e) {
            e.printStackTrace();
        }
    }
}

现在我们的单例又被序列化破坏掉了,解决这个问题的方法也很简单,只要在单例累添加一个方法readResolve(),如下

public class SerializableSingleton implements Serializable {
    private static final SerializableSingleton instance = new SerializableSingleton();
    private SerializableSingleton(){}
    public static SerializableSingleton getInstance() {
        return instance;
    }
    public Object readResolve() {
        return instance;
    }
}

加上方法之后再次执行测试方法输出结果就是true了。为什么加了readResolve()方法就不会被序列化破坏了呢?我们进入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方法,往下找到TC_OBJECT条件下checkResolve(readOrdinaryObject(unshared))
 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);
    }

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

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

为了清晰一些我们另起一个代码窗口,好接着上面的继续看,readOrdinaryObject方法中有一个isInstantiable()方法,就是判断目标类是否有无参构造方法。

boolean isInstantiable() {
    requireInitialized();
    return (cons != null);
}

我们再回到readOrdinaryObject方法继续看,有一段代码desc.hasReadResolveMethod(),readResolveMethod不为空就返回true

boolean hasReadResolveMethod() {
    requireInitialized();
    return (readResolveMethod != null);
}

readResolveMethod是在哪里初始化的呢?通过全局搜索,我们找到下面一段代码

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

就是通过反射找到无参的readResolve方法并且保存下来。我们继续回到readOrdinaryObject方法,看到如果存在无参readResolve方法,则执行下面的方法invokeReadResolve,该方法通过反射调用了单例类的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();
    }
}

好。通过对jdk源码分析我们弄懂了readResolve()方法是怎么解决序列化破坏单例的问题,但是实际上对象创建了两次,只不过新创建的对象没有被返回,而是返回了单例类的readResolve()方法的返回对象。那么这种解决办法也同时存在着多余的内存开销问题,难道就没有办法彻底解决这个问题吗?

  • 注册式单例模式
    • 枚举式单例模式
    • 容器式单例模式

1. 枚举式单例模式

public enum EnumSingleton {
    INSTANCE;
    public static EnumSingleton getInstance() {
        return INSTANCE;
    }
}
public class EnumTest {
    public static void main(String[] args) {
        try {
            EnumSingleton instance = EnumSingleton.getInstance();

            ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("enum.obj"));
            objectOutputStream.writeObject(instance);
            objectOutputStream.flush();
            objectOutputStream.close();

            ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream("enum.obj"));
            EnumSingleton serializableInstance = (EnumSingleton) objectInputStream.readObject();
            objectInputStream.close();
            System.out.println(instance == serializableInstance);//运行结果:true
        }catch (Exception e) {
            e.printStackTrace();
        }
    }
}

运行结果为true,为什么枚举式单例不会被序列化破坏呢?我们再找到上面ObjectInputStream中的readObject0方法,其中有一段代码

case TC_ENUM:
            return checkResolve(readEnum(unshared));
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;
}

可以看到枚举对象是通过Class和类名找到唯一枚举对象,因此不存在被类加载器加载多次。那么枚举单例会不会被反射破坏呢?我们找到java.lang.Enum类:

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

我们写一个测试类尝试用反射破坏枚举单例

public static void main(String[] args) {
    try {
        Constructor constructor = EnumSingleton.class.getDeclaredConstructor(String.class, int.class);
        constructor.setAccessible(true);
        EnumSingleton instance = (EnumSingleton) constructor.newInstance("老船长"1);
    }catch (Exception e) {
        e.printStackTrace();
    }
}

运行结果:Cannot reflectively create enum objects,结果很明显。

java.lang.IllegalArgumentException: Cannot reflectively create enum objects
 at java.lang.reflect.Constructor.newInstance(Constructor.java:417)
 at EnumReflectTest.main(EnumReflectTest.java:15)

Process finished with exit code 0

我们进入Constructor的newInstance方法找一下原因,如果修饰符是Modifier.ENUM就会抛出异常。

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

容器式单例

容器式单例就是提供一个全局Map来存储实例,很简单就不赘述了。

public class SingletonContainer {
    private SingletonContainer() {}
    private static Map<String, Object> ioc = new ConcurrentHashMap<>();
    public static Object getInstance(String className) {
        if(!ioc.containsKey(className)) {
            try {
                ioc.put(className, Class.forName(className).newInstance());
            } catch (Exception e) {
                e.printStackTrace();
            }
        } 
        return ioc.get(className);
    }
}

小结

今天的内容就到这里,我们总结了常见的几种单例模式写法,分析了如何破坏单例及怎么防止被破坏。

看到这里的帅哥美女们帮忙关注一下,期待和你一起学习一起成长。