一个类只允许创建一个对象
一个类只允许创建一个对象(或者实例),那这个类就是一个单例类,这种设计模式就叫作单例模式(Singleton Design Pattern)。
单例模式创建的对象是进程唯一的。
创建时考虑安全问题
- 构造函数需要是
private
访问权限的,这样才能避免外部通过new
创建实例; - 考虑对象创建时的线程安全问题;
使用时考虑性能问题
-
考虑是否支持延迟加载;
延迟加载:真正使用时,才创建实例
-
考虑
getInstance()
性能是否高(是否加锁)。
饿汉式创建方式最简单,但不支持延迟加载
在类加载的时候,instance 静态实例就已经创建并初始化好了,所以,instance 实例的创建过程是线程安全的。不过,这样的实现方式不支持延迟加载。
public class IdGenerator {
private AtomicLong id = new AtomicLong(0);
private static final IdGenerator instance = new IdGenerator();
private IdGenerator() {}
public static IdGenerator getInstance() {
return instance;
}
public long getId() {
return id.incrementAndGet();
}
}
- 若初始化耗时长,提前到程序启动的时候完成,这样就能避免在程序运行的时候,再去初始化导致的性能问题。
- 若实例占用资源多,有问题及早暴露,在程序启动的时候触发报错,可以立即去修复。避免在程序运行一段时间后,突然因为初始化这个实例占用资源过多,导致系统崩溃,影响系统的可用性。
懒汉式创建方式支持延迟加载,但并发度低
public class IdGenerator {
private AtomicLong id = new AtomicLong(0);
private static IdGenerator instance;
private IdGenerator() {}
public static synchronized IdGenerator getInstance() {
if (instance == null) {
instance = new IdGenerator();
}
return instance;
}
public long getId() {
return id.incrementAndGet();
}
}
如果这个单例类偶尔会被用到,那这种实现方式还可以接受。但是,如果频繁地用到,那频繁加锁、释放锁及并发度低等问题,会导致性能瓶颈,这种实现方式就不可取了。
双重检测实现方式既支持延迟加载,又支持高并发
public class IdGenerator {
private AtomicLong id = new AtomicLong(0);
private static IdGenerator instance;
private IdGenerator() {}
public static IdGenerator getInstance() {
if (instance == null) {
synchronized(IdGenerator.class) { // 此处为类级别的锁
if (instance == null) {
instance = new IdGenerator();
}
}
}
return instance;
}
public long getId() {
return id.incrementAndGet();
}
}
只要 instance 被创建之后,即便再调用
getInstance()
函数也不会再进入到加锁逻辑中了。
静态内部类方式类似饿汉式,但又能做到了延迟加载
public class IdGenerator {
private AtomicLong id = new AtomicLong(0);
private IdGenerator() {}
private static class SingletonHolder{
private static final IdGenerator instance = new IdGenerator();
}
public static IdGenerator getInstance() {
return SingletonHolder.instance;
}
public long getId() {
return id.incrementAndGet();
}
}
当外部类 IdGenerator
被加载的时候,并不会创建 SingletonHolder
实例对象。只有当调用 getInstance()
方法时,SingletonHolder
才会被加载,这个时候才会创建 instance。
instance 的唯一性、创建过程的线程安全性,都由 JVM 来保证。所以,这种实现方法既保证了线程安全,又能做到延迟加载。
使用场景:处理资源访问冲突
public class Logger {
private FileWriter writer;
public Logger() {
File file = new File("/Users/wangzheng/log.txt");
writer = new FileWriter(file, true);
}
public void log(String message) {
writer.write(mesasge);
}
}
线程不安全,日志信息互相覆盖
在 Web 容器的 Servlet 多线程环境下,如果两个 Servlet 线程同时分别执行 login()
和 create()
两个函数,并且同时写日志到 log.txt 文件中,
那就有可能存在日志信息互相覆盖的情况。
加对象锁不能解决
通过加对象锁 synchronized(this)
的方式并不能解决:
...
public void log(String message) {
synchronized(this) {
writer.write(mesasge);
}
}
...
-
不同的对象之间并不共享同一把锁。
这种锁是一个对象级别的锁,一个对象在不同的线程下同时调用
log()
函数,会被强制要求顺序执行。但是,不同的对象之间并不共享同一把锁。在不同的线程下,通过不同的对象调用执行log()
函数,锁并不会起作用,仍然有可能存在写入日志互相覆盖的问题。
-
FileWriter
本身就是线程安全的。内部实现中本身就加了对象级别的锁,不同的
Logger
对象不共享FileWriter
对象,所以,FileWriter
对象级别的锁也解决不了数据写入互相覆盖的问题。
单例模式解决线程安全问题
只允许创建一个 Logger
对象,所有的线程共享使用的这一个 Logger
对象,共享一个 FileWriter
对象,而 FileWriter
本身是对象级别线程安全的,也就避免了多线程情况下写日志会互相覆盖的问题。
public class Logger {
private FileWriter writer;
private static final Logger instance = new Logger();
private Logger() {
File file = new File("/Users/wangzheng/log.txt");
writer = new FileWriter(file, true);
}
public static Logger getInstance() {
return instance;
}
public void log(String message) {
writer.write(mesasge);
}
}
加类级别的锁也能解决
让所有的对象都共享同一把锁。这样就避免了不同对象之间同时调用 log()
函数,而导致的日志覆盖问题。
...
public void log(String message) {
synchronized(Logger.class) {
writer.write(mesasge);
}
}
...
其他解决方式
-
分布式锁
实现一个安全可靠、无 bug、高性能的分布式锁,并不是件容易的事情。
-
并发队列
多个线程同时往并发队列里写日志,一个单独的线程负责将并发队列中的数据,写入到日志文件。这种方式实现起来也稍微有点复杂。
使用场景:数据在系统中只应保存一份的全局唯一类
-
配置信息类。
在系统中,我们只有一个配置文件,当配置文件被加载到内存之后,以对象的形式存在,也理所应当只有一份。
-
唯一递增 ID 号码生成器。
如果程序中有两个对象,那就会存在生成重复 ID 的情况,所以,我们应该将 ID 生成器类设计为单例。
单例模式问题多,也被称为反模式
单例对 OOP 特性的支持不友好
单例违背了抽象特性
IdGenerator.getInstance().getId();
违背了基于接口而非实现的设计原则,也就违背了广义上理解的 OOP 的抽象特性。
如果未来某一天,我们希望针对不同的业务采用不同的 ID 生成算法。
比如,订单 ID 和用户 ID 采用不同的 ID 生成器来生成。
为了应对这个需求变化,我们需要修改所有用到 IdGenerator
类的地方,这样代码的改动就会比较大。
单例对继承、多态特性的支持不友好
单例类实现继承、多态,会非常奇怪,会导致代码的可读性变差。
不明白设计意图的人,看到这样的设计,会觉得莫名其妙。
所以,一旦你选择将某个类设计成到单例类,也就意味着放弃了继承和多态这两个强有力的面向对象特性,也就相当于损失了可以应对未来需求变化的扩展性。
单例会隐藏类之间的依赖关系
单例类不需要显示创建、不需要依赖参数传递,在函数中直接调用就可以了。如果代码比较复杂,这种调用关系就会非常隐蔽。
在阅读代码的时候,我们就需要仔细查看每个函数的代码实现,才能知道这个类到底依赖了哪些单例类。
单例对代码的扩展性不友好
我们在系统中创建两个数据库连接池,慢 SQL 独享一个数据库连接池,
其他 SQL 独享另外一个数据库连接池,这样就能避免慢 SQL 影响到其他 SQL 的执行。
如果我们将数据库连接池设计成单例类,显然就无法适应这样的需求变更。所以,数据库连接池、线程池这类的资源池,最好还是不要设计成单例类。
单例对代码的可测试性不友好
单例模式的使用会影响到代码的可测试性。
如果单例类依赖比较重的外部资源,比如 DB,我们在写单元测试的时候,希望能通过 mock 的方式将它替换掉。
而单例类这种硬编码式的使用方式,导致无法实现 mock 替换。
除此之外,如果单例类持有成员变量(比如 IdGenerator
中的 id
成员变量),
那它实际上相当于一种全局变量,被所有的代码共享。如果这个全局变量是一个可变全局变量,也就是说,它的成员变量是可以被修改的,
那我们在编写单元测试的时候,还需要注意不同测试用例之间,修改了单例类中的同一个成员变量的值,从而导致测试结果互相影响的问题。
单例不支持有参数的构造函数
单例类在不同范围内,其唯一性不同
普通的单例类是进程唯一的
单例类中对象的唯一性的作用范围是进程内的,在进程间是不唯一的。
线程唯一的单例类
通过一个 HashMap
来存储对象,其中 key
是线程 ID,value
是对象。这样我们就可以做到,不同的线程对应不同的对象,同一个线程只能对应一个对象。
public class IdGenerator {
private AtomicLong id = new AtomicLong(0);
private static final ConcurrentHashMap<Long, IdGenerator> instances = new ConcurrentHashMap<>();
private IdGenerator() {}
public static IdGenerator getInstance() {
Long currentThreadId = Thread.currentThread().getId();
instances.putIfAbsent(currentThreadId, new IdGenerator());
return instances.get(currentThreadId);
}
public long getId() {
return id.incrementAndGet();
}
}
分布式集群环境下的单例
集群唯一就相当于是进程内唯一、进程间也唯一。不同的进程间共享同一个对象,不能创建同一个类的多个对象。
需要把这个单例对象序列化并存储到外部共享存储区(比如文件)。
进程在使用这个单例对象的时候,需要先从外部共享存储区中将它读取到内存,并反序列化成对象,然后再使用,使用完成之后还需要再存储回外部共享存储区。
为了保证任何时刻,在进程间都只有一份对象存在,一个进程在获取到对象之后,需要对对象加锁,避免其他进程再将其获取。在进程使用完这个对象之后,还需要显式地将对象从内存中删除,并且释放对对象的加锁。
public class IdGenerator {
private AtomicLong id = new AtomicLong(0);
private static IdGenerator instance;
private static SharedObjectStorage storage = FileSharedObjectStorage(/*入参省略,比如文件地址*/);
private static DistributedLock lock = new DistributedLock();
private IdGenerator() {}
public synchronized static IdGenerator getInstance()
if (instance == null) {
lock.lock();
instance = storage.load(IdGenerator.class);
}
return instance;
}
public synchroinzed void freeInstance() {
storage.save(this, IdGeneator.class);
instance = null; //释放对象
lock.unlock();
}
public long getId() {
return id.incrementAndGet();
}
}
// IdGenerator使用举例
IdGenerator idGeneator = IdGenerator.getInstance();
long id = idGenerator.getId();
IdGenerator.freeInstance();