设计模式之美-创建型模式-单例模式

单例模式

一个类只允许创建一个对象(或者实例),那这个类就是一个单例类,这种设计模式就叫作单例设计模式,简称单例模式。

单例的用处

从业务概念上,有些数据在系统中只应该保存一份,就比较适合设计为单例类。比如,系统的配置信息类。除此之外,我们还可以使用单例解决资源访问冲突的问题。

单例的实现

    饿汉式

饿汉式的实现方式,在类加载的期间,就已经将 instance 静态实例初始化好了,所以,instance 实例的创建是线程安全的。不过,这样的实现方式不支持延迟加载实例。

    懒汉式

懒汉式相对于饿汉式的优势是支持延迟加载。这种实现方式会导致频繁加锁、释放锁,以及并发度低等问题,频繁的调用会产生性能瓶颈。

    双重检测

双重检测实现方式既支持延迟加载、又支持高并发的单例实现方式。只要 instance 被创建之后,再调用 getInstance() 函数都不会进入到加锁逻辑中。所以,这种实现方式解决了懒汉式并发度低的问题。

    静态内部类

利用 Java 的静态内部类来实现单例。这种实现方式,既支持延迟加载,也支持高并发,实现起来也比双重检测简单。

    枚举

最简单的实现方式,基于枚举类型的单例实现。这种实现方式通过 Java 枚举类型本身的特性,保证了实例创建的线程安全性和实例的唯一性。

单例存在的问题

    单例对 OOP 特性的支持不友好 单例会隐藏类之间的依赖关系 单例对代码的扩展性不友好 单例对代码的可测试性不友好 单例不支持有参数的构造函数

如何理解单例模式的唯一性?

单例类中对象的唯一性的作用范围是“进程唯一”的。“进程唯一”指的是进程内唯一,进程间不唯一;“线程唯一”指的是线程内唯一,线程间可以不唯一。实际上,“进程唯一”就意味着线程内、线程间都唯一,这也是“进程唯一”和“线程唯一”的区别之处。“集群唯一”指的是进程内唯一、进程间也唯一。

如何实现线程唯一的单例?

我们通过一个 HashMap 来存储对象,其中 key 是线程 ID,value 是对象。这样我们就可以做到,不同的线程对应不同的对象,同一个线程只能对应一个对象。实际上,Java 语言本身提供了 ThreadLocal 并发工具类,可以更加轻松地实现线程唯一单例。

如何实现集群环境下的单例?

我们需要把这个单例对象序列化并存储到外部共享存储区(比如文件)。进程在使用这个单例对象的时候,需要先从外部共享存储区中将它读取到内存,并反序列化成对象,然后再使用,使用完成之后还需要再存储回外部共享存储区。为了保证任何时刻在进程间都只有一份对象存在,一个进程在获取到对象之后,需要对对象加锁,避免其他进程再将其获取。在进程使用完这个对象之后,需要显式地将对象从内存中删除,并且释放对对象的加锁。

如何实现一个多例模式?

“单例”指的是一个类只能创建一个对象。对应地,“多例”指的就是一个类可以创建多个对象,但是个数是有限制的,比如只能创建 3 个对象。多例的实现也比较简单,通过一个Map 来存储对象类型和对象之间的对应关系,来控制对象的个数。

经验分享 程序员 微信小程序 职场和发展