创建式:单例模式

1 基本介绍

​ 单例模式简单来说就是不用创建多个实例

​ 单例模式有三个要点:

  • 某个类只能有一个实例
  • 它必须自行创建这个实例
  • 必须自行向整个系统提供这个实例

​ 这是最简单的设计模式,核心结构只有一个包含称为单例类的特殊类

image-20220314135336298

​ 下面就直接看看单例的几种实现方式把

2 设计分析

2.1 饿汉式单例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
package DesignPatterns.ShangGuiGu.singleton.type1;

public class SingletonTest01 {
public static void main(String[] args) {
Singleton instance = Singleton.getInstance();
Singleton instance2 = Singleton.getInstance();
System.out.println(instance == instance2);
System.out.println(instance.hashCode());
System.out.println(instance2.hashCode());
}
}

// 饿汉式(静态变量)
class Singleton {
// 1. 构造器私有化
private Singleton() {
}

// 2. 本类内部创建对象实例
private final static Singleton instance = new Singleton();

// 3. 提供一个公有的静态方法,返回实例对象
public static Singleton getInstance() {
return instance;
}
}

image-20220314140002765

2.2 懒汉式单例(线程不安全)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
package DesignPatterns.ShangGuiGu.singleton.type3;

public class SingletonTest03 {
public static void main(String[] args) {
Singleton instance = Singleton.getInstance();
Singleton instance2 = Singleton.getInstance();
System.out.println(instance == instance2);
System.out.println(instance.hashCode());
System.out.println(instance2.hashCode());
}
}

class Singleton {
private static Singleton instance;

private Singleton() {}

// 提供一个静态的公有方法,当使用到该方法时,才去创建 instance
// 即懒汉式
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}

2.3 懒汉式单例(线程安全DCL)✨

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package DesignPatterns.ShangGuiGu.singleton.test.test3;

/**
* DCL
*/
public class A {
public static void main(String[] args) {
B b1 = B.getSingleton();
B b2 = B.getSingleton();
System.out.println(b1 == b2);
}
}

class B {
private volatile static B b;
public static B getSingleton() {
if (b == null) {
synchronized (B.class) {
b = new B();
}
}
return b;
}
}

​ 其实可以直接在方法上加锁保证线程安全,但这样导致系统性能降低,使用双重检查锁机制更加适合。注意,这里要使用volatile关键字,volatile关键字会屏蔽Java虚拟机所做的一些代码优化。

2.4 IoDH✨

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package DesignPatterns.ShangGuiGu.singleton;

public class Singleton {
private static class HolderClass {
private final static Singleton instance = new Singleton();
}

public static Singleton getInstance() {
return HolderClass.instance;
}

public static void main(String[] args) {
Singleton s1, s2;
s1 = Singleton.getInstance();
s2 = Singleton.getInstance();
System.out.println(s1 == s2);
}
}

image-20220314141106108

​ 由于静态单例对象没有作为Singleton的成员变量直接实例化,因此类加载时不会实例化Singleton,第一次调用getInstance()时将加载内部类HolderClass,在该内部类中定义了一个static类型的变量instance,此时会首先初始化这个成员变量,由Java虚拟机来保证其线程安全性,确保该成员变量只能初始化一次。由于getInstance()方法没有任何线程锁定,因此其性能不会造成任何影响。

​ 通过IoDH,既可以实现延迟加载,还可以保证线程安全,不影响系统性能,是最好的实现方式

3 总结

优缺点分析

优点:

  1. 单例模式提供了对唯一实例的受控访问
  2. 节约系统资源
  3. 允许可变数目的实例

缺点:

  1. 没有抽象层,因此单例类的扩展有很大的困难
  2. 单例类的职责过重,在一定程度上违背了“单一职责原则”
  3. 如果实例化的共享对象长时间不被利用,系统会认为它是垃圾,会自动销毁并回收资源,下次利用时又将重新实例化,这将导致共享的单例对象状态的丢失。

适用场景

  1. 系统只需要一个实例对象,如系统要求提供一个唯一的序列号生成器或资源管理器,或者需要考虑资源消耗太大而只允许创建一个对象。
  2. 客户调用类的单个实例只允许使用一个公共访问点,除了该公共访问点,不能通过其他途径访问该实例。

4 场景使用

4.1 Spring中的单例模式

​ Spring的依赖注入(包括lazy-init方式)都是发生在 AbstractBeanFactorygetBean 里。 getBeandoGetBean 方法调用 getSingleton 进行bean的创建。lazy-init方式(lazy-init=“true”),在用户向容器第一次索要bean时进行调用;非lazy-init方式(lazy-init=“false”),在容器初始化时候进行调用。

​ 同步线程安全的单例核心代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
public class DefaultSingletonBeanRegistry extends SimpleAliasRegistry implements SingletonBeanRegistry {

// 通过 Map 实现单例注册表
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<String, Object>(64);

public Object getSingleton(String beanName, ObjectFactory<?> singletonFactory) {
Assert.notNull(beanName, "'beanName' must not be null");
synchronized (this.singletonObjects) {
// 检查缓存中是否存在实例
Object singletonObject = this.singletonObjects.get(beanName);
if (singletonObject == null) {
// ...忽略代码
try {
singletonObject = singletonFactory.getObject();
}
catch (BeanCreationException ex) {
// ...忽略代码
}
finally {
// ...忽略代码
}
// 如果实例对象在不存在,我们注册到单例注册表中。
addSingleton(beanName, singletonObject);
}
return (singletonObject != NULL_OBJECT ? singletonObject : null);
}
}

protected void addSingleton(String beanName, Object singletonObject) {
synchronized (this.singletonObjects) {
this.singletonObjects.put(beanName, (singletonObject != null ? singletonObject : NULL_OBJECT));

}
}
}

​ Spring 对 Bean 实例的创建是采用单例注册表的方式进行实现的,而这个注册表的缓存是 ConcurrentHashMap对象。

​ 使用该方式的原因是方便继承。单例注册表的构造函数是protected,可以继承。

参考

Java 静态内部类的加载时机 - は問わない - 博客园 (cnblogs.com)

(28 封私信) 为什么Spring使用注册表来实现单例模式? - 知乎 (zhihu.com)木木甫回答


创建式:单例模式
https://2w1nd.github.io/2022/03/14/设计模式/创建式:单例模式/
作者
w1nd
发布于
2022年3月14日
许可协议