设计模式之单例模式

本文介绍了单例模式,确保一个类只有一个实例并提供全局访问点。阐述了其应用场景、基础结构,详细介绍饿汉式、懒汉式、注册式等多种实现方式及优缺点。还针对new、反射、克隆、序列化等破坏单例的情况给出完善方案,枚举式单例是较优雅的实现。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1 单例模式的应用场景

单例模式(Singleton Pattern)是指确保一个类在任何情况下都绝对只有一个实例,并提供一个全局访问点。单例模式是创建型模式。单例模式在现实生活中应用也非常广泛。例如, 国家主席、公司CEO 、部门经理等。在J2EE标准中,ServletContext、ServletContextConfig 等;在Spring 框架应用中ApplicationContext;数据库的连接池也都是单例形式。

2 基础结构

单例模式的基础结构就三点:

  • 全局实例引用instance。一个静态的引用,用来存放全局唯一的对象引用;
  • 私有构造方法Singleton()。私有构造方法的目的是为了防止有人通过构造方法new新的对象,破坏单例;
  • 暴露一个获取实例的方法getInstance()。为了保证单例的安全性,通常会所有外部能修改对象引用的方式都屏蔽掉,提供一个获取实例的方法对外调用就行了。

3 多种方式介绍

单例模式的实现方式有多种,各有利弊。比较常见的是饿汉式单例和懒汉式单例。

3.1 饿汉式单例

public class HungrySingleton {
    private static final HungrySingleton hungrySingleton = new HungrySingleton();
    private HungrySingleton(){}
    public static HungrySingleton getInstance(){
        return hungrySingleton;
    }
}

饿汉式单例是在类加载的时候就立即初始化,并且创建单例对象。绝对线程安全,在线程还没出现以前就是实例化了,不可能存在访问安全问题。
优点:没有加任何的锁、执行效率比较高,在用户体验上来说,比懒汉式更好。
缺点:类加载的时候就初始化,不管用与不用都占着空间,浪费了内存,有可能占着茅坑不拉屎。

3.2 懒汉式单例

3.2.1 懒汉式的简单实现

public class LazySimpleSingleton {
    private static LazySimpleSingleton lazy = null;

    private LazySimpleSingleton() {
    }

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

写一个线程类ExectorThread 类

public class ExectorThread implements Runnable {
    @Override
    public void run() {
        LazySimpleSingleton singleton = LazySimpleSingleton.getInstance();
        System.out.println(Thread.currentThread().getName() + ":" + singleton);
    }
}

测试

public class SingletonTest {
    public static void main(String[] args) {
        Thread thread1 = new Thread(new ExectorThread());
        Thread thread2 = new Thread(new ExectorThread());
        thread1.start();
        thread2.start();
        System.out.println("end");
    }
}

输出

end
Thread-0:com.hsf.study.homework.designpatterns.singleton.lazy.LazySimpleSingleton@275635de
Thread-1:com.hsf.study.homework.designpatterns.singleton.lazy.LazySimpleSingleton@199f4eb8

一定几率出现创建两个不同结果的情况,意味着上面的单例存在线程安全隐患。为了解决这个问题,我们可以考虑加锁。

3.2.2 双重检查锁实现单例

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实例加了volatile关键字修饰,要理解为什么要加volatile,首先要理解new LazyDoubleCheckSingleton()做了什么。new一个对象有几个步骤:1.看class对象是否加载,如果没有就先加载class对象,2.分配内存空间,初始化实例,3.调用构造函数,4.返回地址给引用。而cpu为了优化程序,可能会进行指令重排序,打乱这3,4这几个步骤,导致实例内存还没分配,就被使用了。所以假设线程A拿到了锁去创建实例,这时候指令重排,先返回了引用地址,但是还没有调用构造函数,此时cpu切换,到了线程B获取实例,发现引用不为空,直接返回引用,接着使用的时候会发现实例并没有初始化,于是报错。使用volatile关键字修饰之后,jvm就不会对lazy实例的new操作进行指令重排。

3.2.3 内部类实现单例

上面使用双重检查锁实现单例已经很完美了,但是,用到synchronized 关键字,总归是要上锁,对程序性能还是存在一定影响的。难道就真的没有更好的方案吗?当然是有的。我们可以从类初始化角度来考虑,看下面的代码,采用静态内部类的方式:

public class LazyInnerClassSingleton {
    private LazyInnerClassSingleton(){}

    /**
     * 每一个关键字都不是多余的
     * static 是为了使单例的空间共享
     * final 保证这个方法不会被重写,重载
     */
    public static final LazyInnerClassSingleton getInstance(){
        //在返回结果以前,一定会先加载内部类
        return LazyHolder.LAZY;
    }
    
    /**
     * 默认不加载内部类
     */
    private static class LazyHolder{
        private static final LazyInnerClassSingleton LAZY = new LazyInnerClassSingleton();
    }
}

这种形式兼顾饿汉式的内存浪费,也兼顾synchronized 性能问题。内部类一定是要在方法调用之前初始化,巧妙地避免了线程安全问题。

我们可以看一下时序图:

1、客户端调用LazyInnerClassSingleton.getInstance(),此时会先判断LazyInnerClassSingleton这个类是否已经加载,如果没有加载则先加载,然后调用getInstance方法;
2、getInstance方法内调用了LazyHolder.LAZY,则此时会先判断LazyHolder这个类是否已经加载,如果没有加载则先加载,并初始化自身的静态属性,此时LAZY通过new LazyInnerClassSingleton()完成了初始化;
3、返回LazyHolder的属性LAZY的引用,最终把引用返回到客户端;

从上面的流程逻辑,我们可以看到,内部类是在方法调用之前初始化,如果在getInstance方法中没有调用LazyHolder.LAZY,那么LazyHolder是不会完成初始化的,巧妙地避免了线程安全问题,同时节省了系统的开销。

3.3 注册式单例

注册式单例又称为登记式单例,就是将每一个实例都登记到某一个地方,使用唯一的标识获取实例。注册式单例有两种写法:一种为容器缓存,一种为枚举登记。

3.3.1 枚举登记

创建枚举类EnumSingleton

public enum EnumSingleton {
    INSTANCE;
    public static EnumSingleton getInstance(){
        return INSTANCE;
    }
}

就这样,很简单的就实现了一个单例。这是依赖了枚举类的一些特性,因为每个枚举元素就是一个实例,所以这里如果写了两个以上的枚举元素,就不能叫单例了,哈哈。

3.3.2 容器缓存

创建ContainerSingleton 类

public class ContainerSingleton {
    private ContainerSingleton(){}
    private static Map<String,Object> ioc = new ConcurrentHashMap<String,Object>();
    public static Object getInstance(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);
            }
        }
    }
}

容器式写法适用于创建实例非常多的情况,便于集中管理。有局限性,如果客户越过容器,自己创建实例,则会破坏单例性。
我们还可以来看看容器式单例在Spring中使用的一个例子:

public abstract class AbstractAutowireCapableBeanFactory extends AbstractBeanFactory
        implements AutowireCapableBeanFactory {
    /** Cache of unfinished FactoryBean instances: FactoryBean name --> BeanWrapper */
    private final Map<String, BeanWrapper> factoryBeanInstanceCache = new ConcurrentHashMap<>(16);
    ...
}

4 完善

基本上,单例模式就上面的几种实现方式了。那么上面的实现是不是就完美了呢?好的代码是要经得起考验的,单例模式的核心里面就体现在“单例”两个字上,那么我们写的单例会被破坏吗?
要破坏单例,首先我们要知道创建一个新对象都有哪些方式:new、反射、克隆、反序列化。

4.1 针对new

普通class类,外部new对象的方式创建新实例,使用private关键字修饰构造方法即可;
枚举类,无法通过构造方法创建新的实例,它的实例都是事先定义好的枚举元素,天然免疫这个问题。

4.2 针对反射

以内部类单例为例,通过反射创建新的实例

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);
    } catch (Exception e) {
        e.printStackTrace();
    }
}

输出结果为false。

从结果上看,我们的单例是被破坏了,那有没有什么办法可以避免这个问题呢?既然是通过反射执行构造方法创建的实例,那么我们可以在构造方法里面加点料

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

再次运行得到以下结果:

 上面时候普通class类的处理方式,那么枚举式单例呢?

public static void main(String[] args) {
    try {
        Class<?> clazz = EnumSingleton.class;
        Constructor c = clazz.getDeclaredConstructor(null);
        c.setAccessible(true);
        Object o1 = c.newInstance();
        Object o2 = c.newInstance();
        System.out.println(o1 == o2);
    } catch (Exception e) {
        e.printStackTrace();
    }
}

 输出结果如下:

说的是没有找到构造方法,那我们看一下java.lang.Enum的源码,查看它的构造方法,只有一个protected的构造方法,代码如下:

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

然后再来做这样一个测试:

public static void main(String[] args) {
    try {
        Class clazz = EnumSingleton.class;
        Constructor c = clazz.getDeclaredConstructor(String.class,int.class);
        c.setAccessible(true);
        EnumSingleton enumSingleton = (EnumSingleton)c.newInstance("hsf",666);
    }catch (Exception e){
        e.printStackTrace();
    }
}

 输出结果如下:

结果里面说的很明白了,Cannot reflectively create enum objects,不能通过发射创建枚举对象!
来看看JDK 源码,进入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()方法中做了强制性的判断,如果修饰符是Modifier.ENUM 枚举类型,直接抛出异常。到这为止,我们是不是已经非常清晰明了呢?枚举式单例也是《Effective Java》书中推荐的一种单例实现写法。在JDK 枚举的语法特殊性,以及反射也为枚举保驾护航,让枚举式单例成为一种比较优雅的实现。

4.3 针对克隆

我们来看一下java.lang.Object里的clone()方法:

protected native Object clone() throws CloneNotSupportedException;

它的修饰符是protected,意味着本类及子类可以调用,那么我们让LazyInnerClassSingleton类实现Cloneable,并重写clone方法,将protected关键字改成public再来测试:

public static void main(String[] args) throws CloneNotSupportedException {
    LazyInnerClassSingleton instance = LazyInnerClassSingleton.getInstance();
    System.out.println(instance);
    System.out.println(instance.clone());
}

输出结果如下:

那么为了避免出现这种情况,我们应该避免暴露相关的克隆方法出去,当然,既然是单例,基本是不会需要克隆这种操作的,所以了解一下就行了。

4.4 针对序列化

当我们将一个单例对象创建好,有时候需要将对象序列化然后写入到磁盘,下次使用时再从磁盘中读取到对象,反序列化转化为内存对象。反序列化后的对象会重新分配内存,即重新创建。那如果序列化的目标的对象为单例对象,就违背了单例模式的初衷,相当于破坏了单例,来看一段代码(在这之前,要先让LazyInnerClassSingleton实现Serializable接口):

public static void main(String[] args) {
    LazyInnerClassSingleton s1 = null;
    LazyInnerClassSingleton s2 = LazyInnerClassSingleton.getInstance();
    FileOutputStream fos = null;
    try {
        fos = new FileOutputStream("LazyInnerClassSingleton.obj");
        ObjectOutputStream oos = new ObjectOutputStream(fos);
        oos.writeObject(s2);
        oos.flush();
        oos.close();
        FileInputStream fis = new FileInputStream("LazyInnerClassSingleton.obj");
        ObjectInputStream ois = new ObjectInputStream(fis);
        s1 = (LazyInnerClassSingleton) ois.readObject();
        ois.close();
        System.out.println(s1);
        System.out.println(s2);
        System.out.println(s1 == s2);
    } catch (Exception e) {
        e.printStackTrace();
    }
}

输出结果如下:

运行结果中,可以看出,反序列化后的对象和手动创建的对象是不一致的,实例化了两次,违背了单例的设计初衷。那么,我们如何保证序列化的情况下也能够实现单例?其实很简单,只需要增加readResolve()方法即可。来看优化代码:

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

    /**
     * 每一个关键字都不是多余的
     * static 是为了使单例的空间共享
     * final 保证这个方法不会被重写,重载
     */
    public static final LazyInnerClassSingleton getInstance(){
        //在返回结果以前,一定会先加载内部类
        return LazyHolder.LAZY;
    }

    /**
     * 默认不加载内部类
     */
    private static class LazyHolder{
        private static final LazyInnerClassSingleton LAZY = new LazyInnerClassSingleton();
    }

    private Object readResolve(){
        return LazyHolder.LAZY;
    }
}

 再看运行结果:

咦?为什么会这样子呢?只是加了一个readResolve()方法,好像也没做什么特殊的操作啊?那我们一起来看看JDK的源码实现就明白了。我们进入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();
            }
        }
    }

我们发现在readObject 中又调用了我们重写的readObject0()方法。进入readObject0()方法,代码如下:

private Object readObject0(boolean unshared) throws IOException {
    ...
    case TC_OBJECT:
        return checkResolve(readOrdinaryObject(unshared));
    ...
}

我们看到TC_OBJECTD 中判断,调用了ObjectInputStream 的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);
        }

        ...

        return obj;
    }

发现调用了ObjectStreamClass 的isInstantiable()方法,而isInstantiable()里面的代码如下:

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

代码非常简单,就是判断一下构造方法是否为空,构造方法不为空就返回true。意味着,只要有无参构造方法就会实例化。
这时候,其实还没有找到为什么加上readResolve()方法就避免了单例被破坏的真正原因。我再回到ObjectInputStream 的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);
        }

        ...

        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) {
                handles.setObject(passHandle, obj = rep);
            }
        }

        return obj;
    }

 判断无参构造方法是否存在之后,又调用了hasReadResolveMethod()方法,来看代码:

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

 逻辑非常简单,就是判断readResolveMethod 是否为空,不为空就返回true。那么readResolveMethod 是在哪里赋值的呢?通过全局查找找到了赋值代码在私有方法ObjectStreamClass()方法中给readResolveMethod 进行赋值,来看代码:

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

 上面的逻辑其实就是通过反射找到一个无参的readResolve()方法,并且保存下来。现在再回到ObjectInputStream 的readOrdinaryObject() 方法继续往下看, 如果readResolve()存在则调用invokeReadResolve()方法,来看代码:

    Object invokeReadResolve(Object obj)
        throws IOException, UnsupportedOperationException
    {
        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();
        }
    }

我们可以看到在invokeReadResolve()方法中用反射调用了readResolveMethod 方法。通过JDK 源码分析我们可以看出,虽然,增加readResolve()方法返回实例,解决了单例被破坏的问题。但是,我们通过分析源码以及调试,我们可以看到实际上实例化了两次,只不过新创建的对象没有被返回而已。那如果,创建对象的动作发生频率增大,就意味着内存分配开销也就随之增大。

以上是普通class类对反序列化破坏单例的处理分析。那么反序列化破坏枚举式单例又会怎样呢?我们不妨再来看一下JDK源码,还是回到ObjectInputStream 的readObject0()方法:

private Object readObject0(boolean unshared) throws IOException {
    ...
    case TC_ENUM:
        return checkResolve(readEnum(unshared));
    ...
}

 我们看到在readObject0()中调用了readEnum()方法,来看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")
                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 对象类找到一个唯一的枚举对象。因此,枚举对象不可能被类加载器加载多次。

5 总结

类型优点缺点
饿汉式单例1.简单,易于理解;
2.安全,不会存在线程问题。
1.过早的创建实例,可能会存在“占着茅坑不拉屎”的情况,浪费系统资源。
双重检查锁单例1.不会过早的过早的创建实例,节约系统资源。1.存在同步锁,如果发生大量阻塞,会造成系统性能下降;
2.相对饿汉式变得更复杂。
内部类单例1.不会过早的过早的创建实例,节约系统资源;
2.不存在同步锁,只有在内部类被调用的时候才会创建对象。
1.通过增加readResolve()方法来避免发序列化被破坏时,还是会再次创建一个新的实例,一定程度上的会浪费系统资源。
枚举登记单例1.实现简单;
2.JDK的宠儿,天生受JDK保护,不会被反射、反序列化等方式破坏单例。
1.要求类必须是枚举类;
2.枚举元素必须有且只有一个。
容器式单例1.集中管理单例,可以更自由、更个性化的管理单例。1.如果客户越过容器,自己创建实例,则会破坏单例性。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值