单例模式

1 实际场景

1.1 读取配置文件的内容

很多应用项目中,都有与应用相关的配置文件,这些配置文件是由项目开发人员自定义的,在里面定义一些应用需要的参数数据。在实际的项目中,这种配置文件多采用 xml 格式的。也有采用 properties 格式(使用 Java 来读取 properties 格式的配置文件比较简单)

考虑如何实现代码来读取配置文件中的内容?

1.2 不使用设计模式的方式

直接读取文件内容,再将文件内容存放在相应的数据对象中。假设系统采用的是 properties 格式的配置文件。

1、直接使用 Java 来读取配置文件

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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
/**
* @author mark.hct
* @version AppConfig.java v 0.1 2023/1/28 14:55 Exp $
* @description
*/
public class AppConfig {
/**
* 用来存放配置文件中参数A的值
*/
private String parameterA;
/**
* 用来存放配置文件中参数B的值
*/
private String parameterB;

public String getParameterA() {
return parameterA;
}

public String getParameterB() {
return parameterB;
}

public AppConfig() {
// 调用读取配置文件的方法
readConfig();
}

/**
* 读取配置文件,把配置文件中的内容读出来设置到属性上
*/
private void readConfig() {
Properties p = new Properties();
InputStream in = null;
try {
in = AppConfig.class.getResourceAsStream("/Test.properties");
p.load(in);
//把配置文件中的内容读出来设置到属性上
this.parameterA = p.getProperty("paramA");
this.parameterB = p.getProperty("paramB");
} catch (IOException e) {
System.out.println("装载配置文件出错了,具体堆栈信息如下:");
e.printStackTrace();
} finally {
try {
assert in != null;
in.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}

只有访问参数的方法,没有设置参数的方法

2、应用的配置文件,名字是 Test.properties

1
2
paramA=a
paramB=b

3、客户端代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* @author mark.hct
* @version Client.java v 0.1 2023/1/28 15:02 Exp $
* @description
*/
public class Client {
public static void main(String[] args) {
AppConfig config = new AppConfig();

String paramA = config.getParameterA();
String paramB = config.getParameterB();

System.out.println("paramA=" + paramA + ",paramB=" + paramB);
}
}

1.3 存在的问题

如上客户端的代码,是通过 new 一个 AppConfig 的实例来得到一个配置文件内容的对象。假设在系统的运行过程中,有许多地方都需要使用此配置文件,即:有很多地方都需要创建该对象的实例。

换句话说,在系统运行期间,系统中会存在很多个・AppConfig 的实例对象,这有什么问题吗?

肯定有问题!每一个 AppConfig 实例对象,里面都封装着配置文件的内容,系统中有多个 AppConfig 实例对象,也就是说系统中会同时存在多份配置文件的内容,这会严重浪费内存资源。如果配置文件内容较少,问题还小一点,如果配置文件内容较大,会造成系统资源严重浪费。事实上,对于 AppConfig 这种类,在运行期间,只需要一个实例对象就够了。

进一步抽象问题:在一个系统运行期间,某个类只需要一个类实例就可以了,那么应该怎么实现呢?

2 解决方案

2.1 单例模式

2.1.1 定义

单例模式(Singleton):保证一个类仅有一个实例,并提供一个可被全局访问的访问点。

2.1.2 单例模式解决问题的思路

通过分析上面的问题,一个类能够被创建多个实例,问题的根源在于类的构造方法是公开的,也就是可以让类的外部来通过构造方法创建多个实例。换句话说,只要类的构造方法能让类的外部访问,就没有办法去控制外部来创建这个类的实例个数。

要想控制一个类只被创建一个实例,那么首要的问题就是要把创建实例的权限收回来,让类自身来负责自己类实例的创建工作,然后由这个类来提供外部可以访问这个类实例的方法,这就是单例模式的实现方式

2.2 结构说明

image-20230128163408983

Singleton:负责创建 Singleton 类自己的唯一实例,并提供一个 getInstance 的方法,让外部来访问这个类的唯一实例。

2.3 代码示例

单例模式的实现又分为两种,一种称为懒汉式,一种称为饿汉式,其实就是在具体创建对象实例的处理上,有不同的实现方式。

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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
/**
* @author mark.hct
* @version Singleton.java v 0.1 2023/1/28 16:44 Exp $
* @description 懒汉式单例实现的示例
*/
public class Singleton {
/**
* 定义一个变量来存储创建好的类实例
*/
private static Singleton uniqueInstance = null;

/**
* 私有化构造方法,好在内部控制创建实例的数目
*/
private Singleton() {
//
}

/**
* 定义一个方法来为客户端提供类实例
*
* @return 一个Singleton的实例
*/
public static synchronized Singleton getInstance() {
//判断存储实例的变量是否有值
if (uniqueInstance == null) {
//如果没有,就创建一个类实例,并把值赋值给存储类实例的变量
uniqueInstance = new Singleton();
}
//如果有值,那就直接使用
return uniqueInstance;
}

/**
* 示意方法,单例可以有自己的操作
*/
public void singletonOperation() {
//功能处理
}

/**
* 示意属性,单例可以有自己的属性
*/
private String singletonData;

/**
* 示意方法,让外部通过这些方法来访问属性的值
*
* @return 属性的值
*/
public String getSingletonData() {
return singletonData;
}
}

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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
/**
* @author mark.hct
* @version Singleton.java v 0.1 2023/1/28 16:48 Exp $
* @description 饿汉式单例实现的示例
*/
public class Singleton {
/**
* 定义一个变量来存储创建好的类实例,直接在这里创建类实例,只会创建一次
*/
private static Singleton uniqueInstance = new Singleton();

/**
* 私有化构造方法,好在内部控制创建实例的数目
*/
private Singleton() {
//
}

/**
* 定义一个方法来为客户端提供类实例
*
* @return 一个Singleton的实例
*/
public static Singleton getInstance() {
//直接使用已经创建好的实例
return uniqueInstance;
}

/**
* 示意方法,单例可以有自己的操作
*/
public void singletonOperation() {
//功能处理
}

/**
* 示意属性,单例可以有自己的属性
*/
private String singletonData;

/**
* 示意方法,让外部通过这些方法来访问属性的值
*
* @return 属性的值
*/
public String getSingletonData() {
return singletonData;
}
}

2.4 重写方案

选择饿汉式的方式来重写示例

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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
/**
* @author mark.hct
* @version Singleton.java v 0.1 2023/1/28 16:54 Exp $
* @description 读取应用配置文件,单例实现
*/
public class SingletonAppConfig {
/**
* 定义一个变量来存储创建好的类实例,直接在这里创建类实例,只会创建一次
*/
private static SingletonAppConfig instance = new SingletonAppConfig();

/**
* 定义一个方法来为客户端提供AppConfig类的实例
*
* @return 一个AppConfig的实例
*/
public static SingletonAppConfig getInstance() {
return instance;
}

/**
* 用来存放配置文件中参数A的值
*/
private String parameterA;
/**
* 用来存放配置文件中参数B的值
*/
private String parameterB;

public String getParameterA() {
return parameterA;
}

public String getParameterB() {
return parameterB;
}

/**
* 私有化构造方法
*/
private SingletonAppConfig() {
//调用读取配置文件的方法
readConfig();
}

/**
* 读取配置文件,把配置文件中的内容读出来设置到属性上
*/
private void readConfig() {
Properties p = new Properties();
InputStream in = null;
try {
in = AppConfig.class.getResourceAsStream("/Test.properties");
p.load(in);
//把配置文件中的内容读出来设置到属性上
this.parameterA = p.getProperty("paramA");
this.parameterB = p.getProperty("paramB");
} catch (IOException e) {
System.out.println("装载配置文件出错了,具体堆栈信息如下:");
e.printStackTrace();
} finally {
try {
assert in != null;
in.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}

客户端代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* @author mark.hct
* @version SingletonClient.java v 0.1 2023/1/28 16:57 Exp $
* @description 测试的客户端
*/
public class SingletonClient {
public static void main(String[] args) {
SingletonAppConfig config = SingletonAppConfig.getInstance();

String paramA = config.getParameterA();
String paramB = config.getParameterB();

System.out.println("paramA=" + paramA + ",paramB=" + paramB);
}
}

3 模式讲解

3.1 认识单例模式

3.1.1 功能

单例模式的功能是用来保证这个类在运行期间只会被创建一个类实例,另外单例模式还提供了一个全局唯一访问这个类实例的访问点,即:getInstance 方法。不管采用懒汉式还是饿汉式的实现方式,这个全局访问点是一样的。

对于单例模式而言,不管采用何种实现方式,它都是只关心类实例的创建问题,并不关心具体的业务功能。

3.1.2 作用范围

目前 Java 里面实现的单例是一个 ClassLoader 及其子 ClassLoader 的范围。因为一个 ClassLoader 在装载饿汉式实现的单例类的时候就会创建一个类的实例

这就意味着如果一个 JVM 里面有很多个 ClassLoader,而且这些 ClassLoader 都装载某个类的话,就算这个类是单例,它也会产生很多个实例。当然,如果一个机器上有多个虚拟机,那么每个虚拟机里面都应该至少有一个这个类的实例,也就是说整个机器上就有很多个实例,这就不算单例。

另外注意:这里讨论的单例模式并不适用于集群环境

3.1.3 命名

一般建议单例模式的方法命名为:getInstance(),该方法的返回类型是单例类的类型。getInstance 方法可以有参数,这些参数可能是创建类实例所需要的参数(大多数情况下是不需要的)

3.2 懒汉式和饿汉式

前面示例中提到了单例的两种模式:懒汉式和饿汉式,本章节会介绍一些实现细节,为了看得更清晰一点,只是实现基本的单例控制部分,不再提供示例的属性和方法。

关于线程安全的问题,在后面会重点分析

3.2.1 懒汉式

1、私有化构造方法

要想在运行期间控制某一个类的实例只有一个,那首先的任务就是要控制创建实例的地方,也就是不能随随便便就可以创建类实例。现在是让使用类的地方来创建类实例,也就是在类外部来创建类实例。那么怎样才能让类的外部不能创建一个类的实例呢?—— 私有化构造方法

1
2
private Singleton() {  
}

2、提供获取实例的方法

构造方法被私有化,则外部创建不了该类的实例就无法调用这个对象的方法,就实现不了功能处理,单例模式可以让这个类提供一个方法来返回实例,以提供给外部使用。

1
2
public Singleton getInstance() {  
}

3、获取实例的方法改为静态方法

随着上一个步骤,产生一个新问题,获取对象实例的这个方法是个实例方法,客户端要想调用这个方法,需要先得到类实例,然后才可以调用,导致死循环问题。

解法是将获取实例的方法改成静态方法 static,这样可以直接通过类来调用此静态方法获取实例。

1
2
public static Singleton getInstance() {  
}

4、定义存储实例的属性

定义好获取实例的方法 getInstance 后,如何实现方法?是否可以直接实现?如下示例代码:

1
2
3
public static Singleton getInstance(){  
return new Singleton();
}

答案是肯定不行。若多处地方调用,会导致产生多个实例。单例模式的做法是用一个属性来记录自己创建好的类实例,当第一次创建过后,就把这个实例保存下来,以后就可以复用这个实例,而不是重复创建对象实例。示例代码如下

1
private Singleton instance = null; 

5、定义静态属性

这个属性变量应该在什么地方用?肯定是第一次创建类实例的地方,也就是在前面那个返回对象实例的静态方法里面使用。

由于要在一个静态方法里面使用,所以这个属性被迫成为一个类变量,要强制加上 static,也就是说,这里并没有使用 static 的特性。示例代码如下:

1
private static Singleton instance = null;

6、控制实例的创建

控制实例的创建较为简单,查看存放实例的属性是否已经赋值,如有值,说明已经创建过,若没有值,应该创建。

1
2
3
4
5
6
7
8
9
10
public static Singleton getInstance() {  
//先判断instance是否有值
if (instance == null) {
//如果没有值,说明还没有创建过实例,那就创建一个
//并把这个实例设置给instance
instance = new Singleton ();
}
//如果有值,或者是创建了值,那就直接使用
return instance;
}

7、完整实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class Singleton {  
//4:定义一个变量来存储创建好的类实例
//5:因为这个变量要在静态方法中使用,所以需要加上static修饰
private static Singleton instance = null;
//1:私有化构造方法,好在内部控制创建实例的数目
private Singleton(){
}
//2:定义一个方法来为客户端提供类实例
//3:这个方法需要定义成类方法,也就是要加static
public static Singleton getInstance(){
//6:判断存储实例的变量是否有值
if(instance == null){
//6.1:如果没有,就创建一个类实例,并把值赋值给存储类实例的变量
instance = new Singleton();
}
//6.2:如果有值,那就直接使用
return instance;
}
}

3.2.2 饿汉式

与懒汉式的实现方式相比,私有化构造方法、提供静态的 getInstance 方法返回实例的方法都一样。区别在于如何实现 getInstance 方法。

Javastatic 的特性为:static 变量在类装载的时候进行初始化;多个实例的 static 变量会共享同一块内存区域。

利用 static 的特性,单例模式的饿汉式的实现方式为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Singleton {  
//4:定义一个静态变量来存储创建好的类实例
//直接在这里创建类实例,只会创建一次
private static Singleton instance = new Singleton();
//1:私有化构造方法,好在内部控制创建实例的数目
private Singleton(){
}
//2:定义一个方法来为客户端提供类实例
//3:这个方法需要定义成类方法,也就是要加static
//这个方法里面就不需要控制代码了
public static Singleton getInstance(){
//5:直接使用已经创建好的实例
return instance;
}
}

无论采用哪一种方式,在运行期间,都只会生成一个实例,而访问这些类的一个全局访问点,为静态的 getInstance 方法。

3.3 关于延迟加载

延迟加载:通俗来讲,就是一开始不要加载资源或者数据,直到到马上就要使用这个资源或者数据时才加载,也称 Lazy Load,这在实际开发中是一种很常见的思想,尽可能的节约资源。

延迟加载在单例模式中的体现:

1
2
3
4
5
6
7
8
9
10
11
public class Singleton {  
//.....
public static Singleton getInstance(){
// 此处体现了延迟加载,马上要使用此实例了,但是无法得知是否实例化,先判断下,若没有则创建一个
if(instance == null){
instance = new Singleton();
}
return instance;
}
//.....
}

3.4 缓存思维

简单来讲,如果某些资源或者数据会被频繁地使用,而这些资源或数据存储在系统外部,比如数据库、硬盘文件等,那么每次操作这些数据的时候都从数据库或者硬盘上去获取,速度会较慢,容易造成性能问题。

一个简单的解决方法:将热点数据缓存到内存里面,每次操作的时候,先到内存里面找,看有没有这些数据,如果有,则直接使用,如果没有那么就获取它,并设置到缓存中,下一次访问的时候就可以直接从内存中获取了。从而节省大量的时间,缓存是一种典型的空间换时间的方案。

缓存思维在单例模式中的体现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Singleton {  
// 此属性就是用来缓存实例
private static Singleton instance = null;
private Singleton(){
}

public static Singleton getInstance(){
// 缓存的实现
if(instance == null){
instance = new Singleton();
}
return instance;
}
}

缓存实现简介

引申一下,在 java 中最常见的缓存实现方式是 Map,基本步骤为:

  1. 先到缓存里面查找,看看是否存在需要使用的数据;
  2. 如果没有找到,那么就创建一个满足要求的数据,然后把这个数据设置回到缓存中,以备下次使用;
  3. 如果找到了相应的数据,或者是创建了相应的数据,那就直接使用这个数据;

示例代码(基本实现,没有考虑缓存的清除、同步等功能)

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
/**
* Java中缓存的基本实现示例
*/
public class JavaCache {
/**
* 缓存数据的容器,定义成Map是方便访问,直接根据Key就可以获取Value了
* key选用String是为了简单,方便演示
*/
private Map<String,Object> map = new HashMap<String,Object>();
/**
* 从缓存中获取值
* @param key 设置时候的key值
* @return key对应的Value值
*/
public Object getValue(String key){
//先从缓存里面取值
Object obj = map.get(key);
//判断缓存里面是否有值
if(obj == null){
//如果没有,那么就去获取相应的数据,比如读取数据库或者文件
//这里只是演示,所以直接写个假的值
obj = key+",value";
//把获取的值设置回到缓存里面
map.put(key, obj);
}
//如果有值了,就直接返回使用
return obj;
}
}

3.5 单例模式的优缺点

3.5.1 时间和空间

懒汉式是典型的时间换空间,每次获取实例都会进行判断,看是否需要创建实例,消耗判断的时间,如果一直没有人使用的话,那就不会创建实例,节约内存空间。

饿汉式是典型的空间换时间,当类装载的时候就会创建类实例,无论是否调用,先创建出来,然后每次调用时,就不需要再判断,节省了运行时间。

3.5.2 线程安全

1、不加同步的懒汉式是线程不安全的

假设:有两个线程 A 和 B,同时调用 getInstance 方法,可能导致并发问题。

1
2
3
4
5
6
public static Singleton getInstance(){  
if(instance == null){
instance = new Singleton();
}
return instance;
}
image-20230129154439726

通过上图的分解描述,明显可以看出,当 A、B 线程并发的情况下,会创建出两个实例来,也就是单例的控制在并发情况下会失效。

如何实现懒汉式的线程安全?可以通过将方法加锁 synchronized 的方式

1
public static synchronized Singleton getInstance(){} 

但是,此种加锁的方式太重了,并发量大时,会导致性能问题。

双重检查加锁

所谓双重检查加锁机制,指的是:不需要每次进入 getInstance 方法都需要同步,而是先不同步,进入方法过后,先检查实例是否存在,如果不存在才进入下面的同步块,这是第一重检查。进入同步块过后,再次检查实例是否存在,如果不存在,就在同步的情况下创建一个实例,这是第二重检查。如此只需要同步一次,从而减少了多次在同步情况下进行判断所浪费的时间。

双重检查加锁机制的实现会使用关键字 volatile

volatile 修饰的变量的值,将不会被本地线程缓存,所有对该变量的读写都是直接操作共享内存,从而确保多个线程能正确的处理该变量。

由于 volatile 关键字可能会屏蔽掉虚拟机中一些必要的代码优化,所以运行效率并不是很高,因此一般建议,没有特别的需要,不要使用。也就是说,虽然可以使用双重加锁机制来实现线程安全的单例,但并不建议大量采用。

实例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class Singleton {  
/**
* 对保存实例的变量添加volatile的修饰
*/
private volatile static Singleton instance = null;
private Singleton(){
}

public static Singleton getInstance(){
//先检查实例是否存在,如果不存在才进入下面的同步块
if(instance == null){
//同步块,线程安全的创建实例
synchronized(Singleton.class){
//再次检查实例是否存在,如果不存在才真的创建实例
if(instance == null){
instance = new Singleton();
}
}
}
return instance;
}
}

这种实现方式既可使实现线程安全的创建实例,又不会对性能造成太大的影响,它只是在第一次创建实例的时候同步,以后就不需要同步了,从而加快运行速度

2、饿汉式是线程安全的

因为虚拟机保证了只会装载一次,在装载类的时候是不会发生并发的。

3.6 其它实现方式

3.6.1 静态内部类方式

懒汉式和饿汉式的两种实现方式都各有优缺点,而是用静态内部类可以达到既能实现延迟加载,又能实现线程安全。

1、基础知识介绍

什么是类级内部类?简单点说,类级内部类指的是:有 static 修饰的成员式内部类。如果没有 static 修饰的成员式内部类被称为对象级内部类

类级内部类相当于其外部类的 static 成分,它的对象与外部类对象间不存在依赖关系,因此可直接创建。而对象级内部类的实例,是绑定在外部对象实例中的。

类级内部类中,可以定义静态的方法,在静态方法中只能够引用外部类中的静态成员方法或者成员变量。类级内部类相当于其外部类的成员,只有在第一次被使用的时候才会被装载。

多线程缺省同步锁:在多线程开发中,为了解决并发问题,主要是通过使用 synchronized 来加互斥锁进行同步控制。但是在某些情况中,JVM 已经隐含地为用户执行了同步,这些情况下就不用用户再来进行同步控制了。这些情况包括:

  1. 由静态初始化器(在静态字段上或 static{} 块中的初始化器)初始化数据时;
  2. 访问 final 字段时
  3. 在创建线程之前创建对象时;
  4. 线程可以看见它将要处理的对象时;

2、实现方式

要想很简单的实现线程安全,可以采用静态初始化器的方式,它可以由 JVM 来保证线程安全。比如前面的 “饿汉式” 实现方式,但是这样一来,不是会浪费一定的空间吗?因为这种实现方式,会在类装载的时候就初始化对象,不管你需不需要。

如果现在有一种方法能够让类装载的时候不去初始化对象,那不就解决问题了?一种可行的方式就是采用类级内部类,在这个类级内部类里面去创建对象实例,这样一来,只要不使用到这个类级内部类,那就不会创建对象实例。从而同时实现延迟加载和线程安全。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class Singleton {  
/**
* 类级的内部类,也就是静态的成员式内部类,该内部类的实例与外部类的实例
* 没有绑定关系,而且只有被调用到才会装载,从而实现了延迟加载
*/
private static class SingletonHolder {
/**
* 静态初始化器,由JVM来保证线程安全
*/
private static Singleton instance = new Singleton();
}
/**
* 私有化构造方法
*/
private Singleton() {
}

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

getInstance 方法第一次被调用的时候,它第一次读取 SingletonHolder.instance,导致 SingletonHolder 类得到初始化;而这个类在装载并被初始化的时候,会初始化它的静态域,从而创建 Singleton 的实例,由于是静态的域,因此只会被虚拟机在装载类的时候初始化一次,并由虚拟机来保证它的线程安全性。

这个实现方式的优势在于,getInstance 方法并没有被同步,并且只是执行一个域的访问,因此延迟初始化并没有增加任何访问成本。

3.6.2 枚举方式

单元素的枚举类型已经成为实现 Singleton 的最佳方法 ——Effective Java

  1. Java 的枚举类型实质上是功能齐全的类,因此可以有自己的属性和方法;
  2. Java 枚举类型的基本思想:通过公有的静态 final 域为每个枚举常量导出实例的类
  3. 从某个角度讲,枚举是单例的泛型化,本质上是单元素的枚举

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* @author mark.hct
* @version SingletonEnum.java v 0.1 2023/1/29 19:40 Exp $
* @description
*/
public enum SingletonEnum {

INSTANCE;

public void operation() {
System.out.println("this is singleton instance");
}

public static void main(String[] args) {
SingletonEnum singleton = SingletonEnum.INSTANCE;
singleton.operation();
}
}

使用枚举来实现单实例控制,会更加简洁,而且无偿的提供了序列化的机制,并由 JVM 从根本上提供保障,绝对防止多次实例化,是更简洁、高效、安全的实现单例的方式。

综上,可以得出结论:枚举是实现单例模式的最佳实践。毕竟使用它全都是优点:

  1. 反射安全
  2. 序列化 / 反序列化安全
  3. 写法简单

3.7 思考单例模式

3.7.1 单例模式的本质

单例模式的本质:控制实例数目

单例模式是为了控制在运行期间,某些类的实例数目只能有一个。可能有人就会想了,那么我能不能控制实例数目为 2 个,3 个,或者是任意多个呢?目的都是 —— 节省资源,有些时候单个实例不能满足实际的需要(并发量),根据测算,3 个实例刚刚好,也就是说,现在要控制实例数目为 3 个,怎么办?

其实思路很简单,就是利用上面通过 Map 来缓存实现单例的示例,进行变形,一个 Map 可以缓存任意多个实例,新的问题就是,Map 中有多个实例,但是客户端调用的时候,到底返回那一个实例呢?,即实例的调度问题,本文不会对调度算法进行展开,示例代码利用简单的循环来展示:

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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
/**
* @author mark.hct
* @version OneExtend.java v 0.1 2023/1/29 19:44 Exp $
* @description
*/
public class OneExtend {
/**
* 定义一个缺省的key值的前缀
*/
private final static String DEFAULT_PREKEY = "Cache";
/**
* 缓存实例的容器
*/
private static Map<String, OneExtend> map = new HashMap<>();
/**
* 用来记录当前正在使用第几个实例,到了控制的最大数目,就返回从1开始
*/
private static int num = 1;
/**
* 定义控制实例的最大数目
*/
private final static int NUM_MAX = 3;

private OneExtend() {}

public static OneExtend getInstance() {
String key = DEFAULT_PREKEY + num;
//缓存的体现,通过控制缓存的数据多少来控制实例数目
OneExtend oneExtend = map.get(key);
if (oneExtend == null) {
oneExtend = new OneExtend();
map.put(key, oneExtend);
}
//把当前实例的序号加1
num++;
if (num > NUM_MAX) {
//如果实例的序号已经达到最大数目了,那就重复从1开始获取
num = 1;
}
return oneExtend;
}

public static void main(String[] args) {
//测试是否能满足功能要求
OneExtend t1 = getInstance ();
OneExtend t2 = getInstance ();
OneExtend t3 = getInstance ();
OneExtend t4 = getInstance ();
OneExtend t5 = getInstance ();
OneExtend t6 = getInstance ();

System.out.println("t1=="+t1);
System.out.println("t2=="+t2);
System.out.println("t3=="+t3);
System.out.println("t4=="+t4);
System.out.println("t5=="+t5);
System.out.println("t6=="+t6);
}
}

执行结果:

1
2
3
4
5
6
t1==pattern.singleton.OneExtend@1d44bcfa
t2==pattern.singleton.OneExtend@266474c2
t3==pattern.singleton.OneExtend@6f94fa3e
t4==pattern.singleton.OneExtend@1d44bcfa
t5==pattern.singleton.OneExtend@266474c2
t6==pattern.singleton.OneExtend@6f94fa3e

3.7.2 使用场景

当需要控制一个类的实例只能有一个,而且客户只能从一个全局访问点访问它时,可以选用单例模式,这些功能恰好是单例模式要解决的问题。

引用