- 来自《秒懂设计模式》
一. 孤独的太阳
我们可以把太阳系看作一个庞大的系统,其中有各种各样的对象存在,丰富多彩的实例造就了系统的美好。这个系统里的某些实例是唯一的,如我们赖以生存的恒星太阳
。
与其他行星或卫星不同的是,太阳是太阳系内唯一的恒星实例。但倘若天上有9个太阳,那么将会带来一场灾难。太阳不多不少仅此一例。
二. 饿汉造日
%%{ init: { 'themeVariables': { 'fontSize': '22px', 'fontFamily': 'Noto Serif SC' } } }%% graph LR T(("饿汉造日
")):::b T --> A(["1. 开始"]):::p T --> B(["2. 构造方法
私有化"]):::p T --> C(["3. 自有永有"]):::p T --> D(["4. 公开访问"]):::p T --> E(["5. 总结"]):::b A --> a("既然太阳系里只有一个太阳,就需要
严格把控太阳实例化的过程。"):::lp a -.-> at(["Public class Sun {} // 一个最简单的Sun类"]):::lg B --> b("太阳只有一个,不能随意创建实例。
但由于Java会自动生成一个无参构造器,
因此必须禁止外部调用构造器*。"):::lp b -.-> bt(["private Sun(){}"]):::lg C --> c("让它在类加载时就自己创建自己,并使其自有永有"):::lp c -.-> ct(["private static final Sun sun = new Sun();"]):::lg D --> d("使用静态方法*getInstance()来获取太阳的单例
对象并将其设置为“public”以暴露给外部使用"):::lp d -.- dt(["public static Sun getInstance(){ return sun; }"]):::lg d -.- d2t["*如同程序入口的静态方法main(),
不需要任何对象引用就能被访问"]:::info T -..-> e("(还可以添加其他功能方法,如发光和发热等)"):::info d --> et("最后,不管谁得到或得到几次,得到的都是同
一个太阳实例,这样就确保了整个太阳系中
恒星太阳的唯一合法性,他人无法伪造。"):::lb E --> e2t("这就是“饿汉模式”(eager initialization):
即在初始阶段(static)就主动进行实例化,并时刻保持
一种渴求的状态(public),无论此单例是否有人使用。"):::lb classDef p fill:#ddaebd classDef b fill:#aab7d2 classDef lp fill:#f4e4e9 classDef lb fill:#d9dfeb classDef lg fill:#cde2da classDef info fill:#f6f6f7,color:#5a5a5f,stroke-dasharray: 3 3, stroke-width: 2px
1. 开始
|
2. 构造方法私有化
接下来我们得确保任何人都不能创建太阳的实例,否则一旦程序员调用代码“new Sun()”,天空就会出现多个太阳,便又需要“后羿”去解决了。
实例化工作完全归属于内部事务,任何外部类都无权干预
|
3. 自有永有
|
关键字 | 说明 |
---|---|
private | 确保太阳实例的私有性、不可见性和不可访问性; |
static | 确保太阳的静态性 ,将太阳放入内存里的静态区,在类加载的时候就初始化了,它与类同在,也就是说它是与类同时期且早于内存堆中的对象实例化的,该实例在内存中永生,内存垃圾收集器(Garbage Collector, GC)也不会对其进行回收; |
final | 确保这个太阳是常量、恒量 ,它是一颗终极的恒星,引用一旦被赋值就不能再修改; |
new | 初始化 太阳类的静态实例,并赋予静态常量sun。 |
4. 公开访问
单例的太阳对象写好了,可一切皆是私有的,外部如何访问?—— 使用静态方法getInstance()来获取太阳的单例对象。
|
三. 懒汉的队伍
graph LR T((懒汉的队伍)):::b T --> A(["1. 用时才创建"]):::p T --> B(["2. 排队
(同步锁)"]):::p T --> C(["3. 需要时再排队
(双检锁)"]):::p A --> a("若无请求就不实例化,
节省内存空间"):::lp a -.-> at(["去掉 final
if(sun == null)"]):::lg B --> b("由于多线程时的缺陷:
请求方法加上同步锁
synchronized"):::lp b -.- bt2(["public static synchronized Sun getInstance() "]):::lg b -.-> bt3("加锁后某线程调用前必须获取同步锁,调用完
会释放锁给其他线程用,也就是给请求排队。"):::info C --> c("为了解决线程阻塞:
使用2个嵌套的判空逻辑"):::lp c -.- ct(["private volatile static Sun sun;
if(sun = null){ synchronized(){
if(sun == null){sun = new Sun();}"]):::lg c -.- ct2["1. 外层放宽入口,保证线程并发的高效性
2. 内层加锁同步,保证实例化的单次运行"]:::info classDef p fill:#ddaebd classDef b fill:#aab7d2 classDef lp fill:#f4e4e9 classDef lb fill:#d9dfeb classDef lg fill:#cde2da classDef info fill:#f6f6f7,color:#433f40,stroke-dasharray: 3 3, stroke-width: 2px
1. 用时才创建
单例的“饿汉模式”,让太阳一开始就准备就绪,随时供应免费日光。
然而,如果始终没人获取日光,那就白造了太阳,白白浪费一块内存区域。类似于商家货品滞销的情况,货架上堆放着商品却没人买,白白浪费空间。因此,商家为了降低风险,规定有些商品必须提前预订,这就是“懒汉模式
”(lazy initialization)。
|
关键字 | 说明 |
---|---|
去掉 final | 一开始并没有造太阳,所以去掉了关键字final。 |
用时才实例化 | 仅在某线程第一次调用第9行的getInstance()方法时才会运行对太阳进行 实例化的逻辑代码,之后再请求就直接返回此实例了。 ❗️缺点:初次请求时速度较之前的饿汉初始化模式慢,因为要消耗CPU资源去临时造这个太阳。 |
2. 排队
因为上述程序在多线程模式下有缺陷:并发请求时判空逻辑会同时成立,导致多次实例化太阳、多次赋值,违背单例理念。
|
关键字 | 说明 |
---|---|
synchronized | 将太阳类Sun中第9行的getInstance()改成了同步方法,如此可避免多线程陷阱。 |
3. 需要时再排队
如果仅为了实例化一个单例对象,直接加锁排队,使用synchronized让所有请求排队等候:会造成线程阻塞,资源与时间被白白浪费。
|
关键字 | 说明 |
---|---|
去掉 final | 第3行对sun变量的定义不再使用final关键字,说明它不再是常量,而是需要后续赋值的变量 ; |
volatile | 关键字volatile对静态变量的修饰则能保证sun变量值在各线程访问时的同步性、唯一性 。 |
去掉入口的同步锁 | 对于第9行的getInstance()方法,去掉方法上的关键字synchronized,使大家都能同时进入方法并对其进行开发。 |
判空逻辑1 | 有些人(线程)起早就是为了观看日出,那么这些人会通过第10行的判空逻辑进入观日台。 |
判空逻辑2 | 在第12行我们又进行一次判空逻辑,这就意味着只有队伍中的第一个人造了太阳,有幸看到了日出的第一缕阳光,而后面的人则统统离开,直到第17行得到已经造好的太阳。 |
最后:太阳高高升起,实例化操作完毕,起晚的人们都无须再进入观日台,直接获取太阳实例即可,温暖阳光普照大地。
四. 大道至简
相比“懒汉模式”,其实更常用“饿汉模式
”,因为这个单例迟早是要被实例化占用内存的,延迟懒加载的意义并不大,加锁解锁反而是一种资源浪费,同步更是会降低CPU的利用率,使用不当的话反而会带来不必要的风险。越简单的包容性越强,而越复杂的反而越容易出错。
单例模式的类结构:
classDiagram class Singleton{ - instance : singleton - Sigleton() + getInstance() Singleton } Singleton --> Singleton
Singleton(单例):包含一个自己的类实例的属性,并把构造方法用privat关键字隐藏起来,对外只提getInstance()方法以获得这个单例对象。