随便问一个程序员,让他说一说最熟悉的 3 种设计模式,单例模式肯定是其中一个。
那么,我们真的理解单例模式了吗?下面我们围绕以下几个问题展开:
- 为什么要使用单例?
- 单例存在哪些问题?
- 单例与静态类的区别?
- 有何替代的解决方案?
为什么使用单例
单例设计模式(Singleton Design Pattern):一个类只允许创建一个对象(或者实例),那这个类就是一个单例类,这种设计模式就叫作单例设计模式,简称单例模式。
为什么我们需要单例这种设计模式?它能解决哪些问题?
处理资源访问冲突
我们实现一个日志打印类Logger
public class Logger {
private FileWriter writer;
public Logger() {
File file = new File("/Users/dxyin/log.txt");
writer = new FileWriter(file, true); //true表示追加写入
}
public void log(String message) {
writer.write(message);
}
}
// Logger类的应用示例:
public class UserController {
private Logger logger = new Logger();
public void login(String username, String password) {
// ...省略业务逻辑代码...
logger.log(username + " logined!");
}
}
其他类在同时调用log()方法,会发生日志覆盖的情况,也就是会发生访问资源冲突。
当然,我们可以在log()方法上加上类锁,相对而言,单例模式的解决思路就简单一些了。单例模式相对于之前类级别锁的好处是,不用创建那么多 Logger 对象,节省内存和其他资源。
表示全局唯一类
从业务概念上,如果有些数据在系统中只应保存一份,那就比较适合设计为单例类。
比如,配置信息类,在系统中只有一份,比如还有唯一递增 ID 号码生成器等。
此处省略代码,具体实现见单例的多种实现方式。
如何实现单例
要实现一个单例,我们需要关注的点无外乎下面几个:
- 构造函数需要是 private 访问权限的,这样才能避免外部通过 new 创建实例;
- 考虑对象创建时的线程安全问题;
- 考虑是否支持延迟加载;
- 考虑 getInstance() 性能是否高(是否加锁)。
饿汉式
饿汉式的实现方式比较简单。在类加载的时候,instance 静态实例就已经创建并初始化好了,所以,instance 实例的创建过程是线程安全的。
// Id生成器
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();
}
}
静态内部类
比双重检测更加简单的实现方法,那就是利用 Java 的静态内部类。它有点类似饿汉式,但又能做到了延迟加载。
public class IdGenerator {
private AtomicLong id = new AtomicLong(0);
private IdGenerator() {}
// 静态内部类,含静态instance,延迟加载
private static class SingletonHolder{
private static final IdGenerator instance = new IdGenerator();
}
// 获取实例是从静态内部类获取
public static IdGenerator getInstance() {
return SingletonHolder.instance;
}
public long getId() {
return id.incrementAndGet();
}
}
枚举
最简单的实现方式,基于枚举类型的单例实现。这种实现方式通过 Java 枚举类型本身的特性,保证了实例创建的线程安全性和实例的唯一性。
public enum IdGenerator {
INSTANCE;
private AtomicLong id = new AtomicLong(0);
public long getId() {
return id.incrementAndGet();
}
}
单例存在的问题
- 单例对 OOP 特性的支持不友好
- 单例会隐藏类之间的依赖关系
- 单例对代码的扩展性不友好
- 单例对代码的可测试性不友好
- 单例不支持有参数的构造函数
替代方案
如果单例类并没有后续扩展的需求,并且不依赖外部系统,那设计成单例类就没有太大问题。
实际上,类对象的全局唯一性可以通过多种不同的方式来保证。
我们既可以通过单例模式来强制保证,也可以通过工厂模式、IOC 容器(比如 Spring IOC 容器)来保证。
总结
单例模式 | 饿汉式 | 懒汉式 | 双重检测 | 静态内部类 | 枚举 |
---|---|---|---|---|---|
原理 | 类加载的时候,instance 静态实例就已经创建并初始化 | 获取实例的时候加对象锁 | 获取实例的时候两次检测 | 增加一个静态内部类去实例化 | 枚举类 |
优点 | 简单,线程安全 | 延迟加载 | 性能高、延迟加载 | 线程安全、延迟加载 | 简单 |
缺点 | 不支持延迟加载 | 性能低 |
文档信息
- 本文作者:yindongxu
- 本文链接:https://iceblow.github.io/2022/03/30/%E5%8D%95%E4%BE%8B%E6%A8%A1%E5%BC%8F/
- 版权声明:自由转载-非商用-非衍生-保持署名(创意共享3.0许可证)