Java中的单例模式详解

在很久以前写的23种设计模式归纳里面,只是大致地描述了各个设计模式。实际上,单例模式存在许多实现方法和演变,并且涉及到较多的知识点。这篇博客就把单例模式相关的内容归纳一下。

本文参考和代码来源:《Spring 5核心原理与30个类手写实战》

单例模式的应用场景

单例模式 (Singleton Pattern) 是指确保一个类在任何情况下都绝对只有一个实例,并提供一个全局访问点。 单例模式是创建型模式。J2EE标准中的ServletContextServletContextConfig 等、Spring 框架应用中的ApplicationContext、数据库的连接池等都是单例形式。

使用场景:
1. 当类只有一个实例且客户可以从一个众所周知的访问点访问它
2. 当这个唯一实例应该是通过子类化可扩展的,且客户应该无需更改代码就能使用一个扩展的实例。

优点:
1. 对唯一实例的受控访问
2. 缩小命名空间,避免命名污染
3. 允许单例有子类
4. 允许可变数目的实例,基于单例模式可以进行扩展,使用与控制单例对象相似的方法获得指定个数的实例对象,既节约了系统资源,又解决了由于单例对象共享过多有损性能的问题

单例模式的分类

饿汉式单例模式

饿汉式单例模式在类加载的时候就立即初始化,并且创建单例对象。不会出现多线程下的访问安全问题(不会因为多线程而创建多个实例)。

  • 优点:没有加任何锁、执行效率比较高,用户体验比懒汉式单例模式更好。
  • 缺点:类加载的时候就初始化,不管用与不用都占着空间,浪费了内存。

Spring中IoC容器ApplicationContext本身就是典型的饿汉式单例模式。

饿汉式单例模式的实现方法:

public class HungrySingleton {
    //静态不可变的单例对象
    private static final HungrySingleton hungrySingleton = new HungrySingleton();
    //构造函数私有化
    private HungrySingleton(){}
    //获取单例对象的入口
    public static HungrySingleton getInstance(){
        return hungrySingleton;
    }
}

虽然通常该类是在调用getInstance方法时被装载的,但不能确定是否有其他方式导致类装载(或许是其他静态方法),也就是说该单例对象不一定是在调用getInstance方法的时候被初始化的,无法达到懒加载的效果。

饿汉式单例模式的另一种写法:

public class HungryStaticSingleton {
    private static final HungryStaticSingleton hungrySingleton;
    //静态代码块
    static {
        hungrySingleton = new HungryStaticSingleton();
    }
    private HungryStaticSingleton(){}
    public static HungryStaticSingleton getInstance(){
        return hungrySingleton;
    }
}

实际上两种写法没有太大的差别,都是在<cinit>方法中被初始化的。

懒汉式单例模式

懒汉式单例模式的特点是:只有外部类第一次调用获取单例对象的入口方法时,该对象才会被初始化。

下面是懒汉式单例模式的一种写法:(已被淘汰,不能使用)

public class LazySimpleSingleton{
    //私有化构造方法
    private LazySimpleSingleton(){}

    private static LazySimpleSingleton lazy = null;

    public static LazySimpleSingleton getInstance(){
        //仅在第一次调用getInstance方法的时候进行初始化
        if(lazy == null){
            lazy = new LazySimpleSingleton();
        }
        return lazy;
    }
}

这种写法存在线程访问安全问题:若多个线程先后进入getInstance方法,但还没创建一个实例,此时lazy == null,接着就会创建多个实例。

为了解决线程安全问题,就可以在方法上加上synchronized关键字。

public class LazySimpleSingleton {
    private LazySimpleSingleton(){}

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

但是synchronized修饰的方法可能导致大量线程在获取单例对象时阻塞,效率较低,下面又有一种改进的方法,就是著名的双重检查锁定

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

线程先判断一遍lazy是否为null,如果为null获取锁,防止在方法级别进行锁定,当确定该对象为空的时候再去争得锁。

但是synchronized总是会引起阻塞,有没有不需要synchronized实现的方法呢?

下面这种方法利用静态内部类的机制,避免了线程安全问题,也避免了synchronized引起的效率过低:

public class LazyInnerClassSingleton {

    private LazyInnerClasssingleton(){}

    public static final LazyInnerClassSingleton getInstance(){
        return LazyHolder.LAZY;
    }

    private static class LazyHolder{
        private static final LazyInnerClassSingleton LAZY = new LazyInnerClassSingleton();
    }
}

为什么这种方式可以实现懒加载和线程安全呢?线程安全的原因是因为该单例对象只会在内部类被装载的时候被初始化一次,没有其他的初始化方法,这点就类似于饿汉模式,而懒加载是因为该内部类只有在第一次调用getInstance的时候被加载,不会受其他方法的影响。

可以注意到,以上几个方法是依次改进的,虽然说最后一个方法最好,但是其他几种方法的思想也需要掌握。

反射破坏单例

单例模式是可以被破坏的,最容易想到的是,既然反射模式只是将构造方法私有化了,那么是否可以使用反射强制调用构造方法

下面是通过反射破坏单例模式的方法:

public class LazyInnerClassSingletonTest {
    public static void main(String[] args) {
        try{
            Class<?> clazz = LazyInnerClassSingleton.class;
            Constructor C = clazz.getDeclaredConstructor(null);

            //将构造器的访问权限强制设置为可访问的
            C.setAccessible(true);

            //创建了两个实例对象
            Object o1 = c.newInstance();
            Object o2 = c.newInstance();
            System.out.println(o1 == o2);//输出true
        } catch (Exception e){
            e.printStackTrace();
        }
    }
}

那么为了防止反射破坏单例模式(是不是挺无聊的),可以在构造方法内进行校验

public class LazyInnerClassSingleton {
    private LazyInnerClassSingleton(){
        if(LazyHolder.LAZY != null){
            throw new RuntimeException("不允许创建多个实例");
        }
    }

    public static final LazyInnerClassSingleton getInstance(){
        return LazyHolder.LAZY; 
    }

    private static class LazyHolder{
        private static final LazyInnerClassSingleton LAZY = new LazyInnerClassSingleton();
    }
}

浅克隆破坏单例

如果单例的类实现了Clonable接口,并且通过浅克隆重写clone方法,那么意味着浅克隆会破坏单例模式。实际上防止克隆破坏单例模式的解决思路非常简单,禁止浅克隆便可。要么我们的单例类不实现Cloneable接口,要么我们重写clone()方法,在clone()方法中返回单例对象即可。

序列化破坏单例

一个单例对象创建好后,有时候需要将对象序列化然后写入磁盘,下次使用时再从磁盘中读
取对象并进行反序列化,将其转化为内存对象。反序列化后的对象会重新分配内存,即重新创建。如果序列化的目标对象为单例对象,就违背了单例模式的初衷,相当于破坏了单例。

比如,实现了Serializable接口:

public class SeriableSingleton implements Serializable {
    public final static SeriableSingleton INSTANCE = new SeriableSingleton();
    private SeriableSingleton(){}
    public static SeriableSingleton getInstance(){
        return INSTANCE;
    }
}

那么如何避免序列化破坏单例模式呢?

public class SeriableSingleton implements Serializable {
    public final static SeriableSingleton INSTANCE = new SeriableSingleton(); 

    private SeriableSingleton(){}

    public static SeriableSingleton getInstance(){
        return INSTANCE;
    }
    private Object readResolve(){
        return INSTANCE;
    }
}

这个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 {
        //通过readObject0获取对象
        Object obj = readObject0(false);
        handles.markDependency(outerHandle, passHandle);
        ClassNotFoundException ex = handles.lookupException(passHandle);
        if (ex != null) {
            throw ex;
        }
        if (depth == 0) {
            vlist.doCallbacks();
            freeze();
        }
        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
{
    //...

    //创建实例
    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);
    }

    //...

    //检查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);
        }
    }

    //...

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

readResolveMethod在ObjectStreamClass的构造方法中被赋值:

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

它会检查对象的readResolve方法是否为空,如果不为空,根据上面的逻辑则会调用invokeReadResolve方法,它会通过反射调用该readResolve方法,于是就获取了单例对象。

登记式单例模式

登记式单例模式,或者说注册式单例模式。为什么不把它放到单例模式的类别里面呢?因为它其实是属于单例模式的一种拓展,实际上并不是严格的单例了。

登记式单例模式是将一个实例都登记到某一个地方,使用唯一的标识获取实例。注册式单例模式有两种:一种为枚举式单例模式,另一种为容器式单例模式

枚举式单例模式

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

这样获取的INSTANCE就是一个单例了,可以进行测试,原理就是枚举的实现机制,使得每个枚举对象都是唯一不变的。枚举的实现机制见:深入理解java虚拟机第三版读书笔记10

这样是实现的单例模式,但可以通过拓展多个枚举对象使得可以通过枚举名获取对应的对象。(实际上是多例了)

容器式单例

看到容器会不会想到IoC容器?其实这就是IoC容器的基本原理:

public class ContainerSingleton {

    private ContainerSingleton(){}

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

    public static Object getBean(String className){
        synchronized (ioc) {
            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);
            }
        }
    }
}

这是一个非常简单的实现,就是通过ConcurrentHashMapBean Id绑定一个Bean对象,实际Spring的实现非常复杂。

原创文章,作者:彭晨涛,如若转载,请注明出处:https://www.codetool.top/article/java%e4%b8%ad%e7%9a%84%e5%8d%95%e4%be%8b%e6%a8%a1%e5%bc%8f%e8%af%a6%e8%a7%a3/