🍧 Peach

蜜桃学代码

🌷《秒懂设计模式》—— 单例模式

一. 孤独的太阳

我们可以把太阳系看作一个庞大的系统,其中有各种各样的对象存在,丰富多彩的实例造就了系统的美好。这个系统里的某些实例是唯一的,如我们赖以生存的恒星太阳

与其他行星或卫星不同的是,太阳是太阳系内唯一的恒星实例。但倘若天上有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. 开始

public class Sun {

}


2. 构造方法私有化

接下来我们得确保任何人都不能创建太阳的实例,否则一旦程序员调用代码“new Sun()”,天空就会出现多个太阳,便又需要“后羿”去解决了。

实例化工作完全归属于内部事务,任何外部类都无权干预

public class 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 class Sun {

private static final Sun sun = new Sun(); // 自有永有的太阳单例

private Sun(){ // 构造方法私有化

}

public static Sun getInstance(){ // 阳光普照,方法公开化
return sun;
}

}




三. 懒汉的队伍

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)。

public class Sun {

private static Sun sun; // 这里不进行实例化

private Sun(){ // 构造方法私有化

}

public static Sun getInstance() {
if(sun == null) { // 如果无日才造日
sun = new Sun();
}
return sun;
}
}
关键字 说明
去掉 final 一开始并没有造太阳,所以去掉了关键字final。
用时才实例化 仅在某线程第一次调用第9行的getInstance()方法时才会运行对太阳进行
实例化的逻辑代码,之后再请求就直接返回此实例了。

❗️缺点:初次请求时速度较之前的饿汉初始化模式慢,因为要消耗CPU资源去临时造这个太阳。


2. 排队

因为上述程序在多线程模式下有缺陷:并发请求时判空逻辑会同时成立,导致多次实例化太阳、多次赋值,违背单例理念。

public class Sun {
private static Sun sun; // 这里不进行实例化

private Sun(){ // 构造方法私有化

}

public static synchronized Sun getInstance() { // 此处加入同步锁
if(sun == null) { // 如果无日才造日
sun = new Sun();
}
return sun;
}
}
关键字 说明
synchronized 将太阳类Sun中第9行的getInstance()改成了同步方法,如此可避免多线程陷阱。


3. 需要时再排队

如果仅为了实例化一个单例对象,直接加锁排队,使用synchronized让所有请求排队等候:会造成线程阻塞,资源与时间被白白浪费。

public class Sun {
private volatile static Sun sun;

private Sun() { // 构造方法私有化

}

public static Sun getInstance() { // 华山入口
if(sun = null){
synchronized(Sun.class){ // 观日者进行排队
if(sun == null){
sun = new Sun(); // 只有排头兵造了太阳,旭日东升
}
}
}
return sun; // ……阳光普照,其余人不必再造日
}

}
关键字 说明
去掉 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()方法以获得这个单例对象。