持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第21天,点击查看活动详情
概念
单例模式(Singleton)是一种非常简单且容易理解的设计模式。
顾名思义,单例即单一的实例,确切地讲就是指在某个系统中只存在一个实例,同时提供集中、统一的访问接口。
实现
太阳是太阳系内唯一的恒星实例,那么本文我们就以太阳为例来介绍单例。
饿汉模式
1. 构造类
首先我们先构造一个太阳的类。
public class Sun {
}
太阳类 Sun 中目前什么都没有。接下来我们得确保任何人都不能创建太阳的实例,否则一旦有同学调用 new Sun(),那么程序中就会存在多个 Sun 实例了。
这里有同学可能会疑惑,我们并没有实现构造器,为什么 Sun 类还是会被实例化呢?原始是 Java 会自动为类加上一个无参构造器。
2. 构造方法私有化
为了防止太阳实例泛滥,我们必须禁止外部调用构造器,即我们要实现构造方法私有化,代码如下所示:
public class Sun {
private Sun() {// 构造方法私有化
}
}
在上述代码中,我们将太阳类 Sun 的构造方法设为 private,使其私有化。这样太阳类的实例化工作完全属于内部事务,不会被任何外部类创建。既然如此,Sun 类的实例化就交由我们自己了。
3. 永久实例
public class Sun {
private static final Sun sun = new Sun(); // 自有永久的单例
private Sun() {// 构造方法私有化
}
}
private关键字确保太阳实例的私有性、不可见性和不可访问性;static关键字确保太阳的静态性,将太阳放入内存里的静态区,在类加载的时候就初始化了,它与类同在,也就是说它是与类同时期且早于内存堆中的对象实例化的,该实例在内存中永生,内存垃圾收集器(Garbage Collector,GC)也不会对其进行回收;final键字则确保这个太阳是常量,引用一旦被赋值就不能再修改;new关键字初始化太阳类的静态实例,并赋予静态常量 sun。
4. 外部访问
单例的太阳对象写好了,可一切皆是私有的,外部怎样才能访问它呢?因此我们需要一个静态方法 getInstance() 来获取太阳的单例对象,同时将其设置为 public 以暴露给外部使用,
public class Sun {
private static final Sun sun = new Sun(); // 自有永久的单例
private Sun() {// 构造方法私有化
}
public static Sun getInstance() { // 公开实例
return sun;
}
}
如此一来,太阳类实例就算完成了。对外部来说只要调用 Sun.getInstance() 就可以得到太阳对象了,并且不管谁得到,或是得到几次,得到的都是同一个太阳实例。
懒汉模式
1. 单线程
上面我们已经学会了单例模式的“恶汉模式”。让太阳类一开始就准备就绪,然而如果始终没有人获取太阳类的话,那么岂不是白白实例化太阳类,浪费一个内存了。
这个时候就可以借用懒汉模式来构造单例,即在需要的时候构造。
public class Sun {
private static final Sun sun = new Sun(); // 自有永久的单例
private Sun() {// 构造方法私有化
}
public static Sun getInstance() { // 公开实例
if (sun == null) { // 没有才构造
sun = new Sun();
}
return sun;
}
}
这样的好处是如无请求就不实例化,节省了内存空间;而坏处是第一次请求的时候速度较之前的饿汉初始化模式慢,因为要消耗 CPU 资源去临时造这个太阳(即使速度快到可以忽略不计)。
2. 多线程
上面的程序逻辑看似没问题,但其实在多线程模式下是有缺陷的。试想如果是并发请求的话,if (sun == null) 的判空逻辑就会同时成立,这样就会多次实例化太阳,并且对 sun 进行多次赋值(覆盖)操作,这违背了单例的理念。
那我们再来改良一下,把请求方法加上 synchronized(同步锁)让其同步,如此一来,某线程调用前必须获取同步锁,调用完后会释放锁给其他线程用,也就是给请求排队,一个接一个按顺序来.
public class Sun {
private static final Sun sun = new Sun(); // 自有永久的单例
private Sun() {// 构造方法私有化
}
public static Synchronized Sun getInstance() { // 加入同步锁
if (sun == null) { // 没有才构造
sun = new Sun();
}
return sun;
}
}
这样我们就可以避免多线程问题,但同时也会引入新的问题。即资源浪费。
当线程调用 Sun 方法时,还没有进入方法内部,就一直加锁排队,这样就会造成线程阻塞,资源与时间被白白浪费。因此我们需要保证多线程并发下逻辑的正确性,Synchronized 关键字加的位置要合理。其代码如下:
public class Sun {
private volatile static final Sun sun;
private Sun() {// 构造方法私有化
}
public static Sun getInstance() {
Synchronized(Sun.class){
if (sun == null) { // 没有才构造
sun = new Sun();
}
}
return sun;
}
}
我们来看一下上面的代码:
- 首先 sun 变量不在是常量,而是需要后续赋值的变量;
- 关键字 volatile 对静态变量的修饰则能保证变量值在各线程访问时的同步性、唯一性。
- 我们去掉了方法上的关键字 synchronized,使大家都可以同时进入方法并对其进行开发。
- 同步块保证我们每次只有一个线程可以进入。
推荐用法
相比“懒汉模式”,其实在大多数情况下我们通常会更多地使用“饿汉模式”,原因在于这个单例迟早是要被实例化占用内存的,延迟懒加载的意义并不大,加锁解锁反而是一种资源浪费,同步更是会降低 CPU 的利用率,使用不当的话反而会带来不必要的风险。越简单的包容性越强,而越复杂的反而越容易出错。
参考文档
- 《秒懂设计模式》—— 刘韬