设计模式之单例模式

概念定义
1
2
3
4
5
6
7
定义:单例模式是指某一个类运行过程中只有一个实例产生,且由这个类自己产生,然后对外提供一个可以访问他的全局访问方法。
作用:提供唯一一个实例,避免资源的抢夺。常见于以下的场景中:
* 资源共享:单例模式可以用于管理共享的系统资源,例如数据库连接池、线程池或缓存。通过使用单例模式,可以确保只有一个实例存在,从而避免资源竞争和浪费。
* 配置信息:当你需要在应用程序中保存全局配置信息时,可以使用单例模式。单例对象可以加载配置文件或从数据库中读取配置信息,并在应用程序的其他部分提供访问。
* 日志记录器:在日志记录器中使用单例模式可以确保在整个应用程序中只有一个日志记录器实例。这样可以避免创建多个日志记录器对象,并统一管理日志的写入和输出。
* 对象缓存:单例模式可以用于实现对象的缓存。当你需要频繁创建和销毁对象,并希望在创建对象时重用已有对象时,可以使用单例模式来实现对象的缓存功能。

实现方式之概述

单例模式有很多种的实现方式,这里仅介绍:饿汉式、懒汉式、双重检查锁单例模式、枚举单例模式。详情如下:

  • 饿汉式:实例创建时机在类加载过程之中。当一个人很饥饿的时候,立马就想吃到东西,所以在加载过程中就进行创建实例了。

  • 懒汉式:实例创建时机在类加载过程之后(这个实例需要被用到的时候才去创建对应的实例)。当一个人很懒惰的时候,只有到了必须的时候才会进行相应的活动,所以在被引用的时候才会去创建实例。

  • 双重检查锁单例模式: 双重检查锁单例模式通过使用双重检查锁机制来实现懒加载和线程安全。在这种模式中,首先检查实例是否已经被创建,如果尚未创建,则使用同步块的方式创建实例。然后再次检查实例是否已经被创建,以防止多个线程同时创建实例。该方式避免了对整个方法进行同步,提高了性能,具有**==延迟加载====线程安全==**的特性。

  • 枚举单例模式:

    1. 线程安全:枚举单例模式在Java中天然地是线程安全的。枚举实例的创建由JVM保证,在任何情况下都只会创建一个实例,不需要担心线程安全问题。

    2. 防止反序列化创建新实例:枚举单例模式默认情况下防止了通过反序列化创建新的实例。因为枚举类的实例是有限且预定义的,所以不会发生创建新实例的情况。

    3. 简洁明了:枚举单例模式的实现非常简洁,代码量很少。而且枚举类提供了序列化、线程安全和保证单例的功能,无需开发人员手动编写这些逻辑。

实现方式之代码
  1. 饿汉式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package com.jw.cloud.modulepattern.singleton;
/**
* @description: 饿汉式的单例模式
* 在类加载过程的时候,就将实例创建出来了,不需要等到被引用的时候在创建,如果一直没被用上,消耗内存占用
* @author: joywu
* @date:2021-06-08 22:23:45
*/
public class Singleton1 {
/**
* 创建一个实例
*/
private static final Singleton1 instance = new Singleton1();
private Singleton1() {}
/**
* 获取到实例对象
*/
public static Singleton1 getInstance() {
return instance;
}
}
  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
package com.jw.cloud.modulepattern.singleton;
/**
* 懒汉式单例模式:
* 1.私有构造方法
* 2.提供该类的静态变量,不是马上创建
* 3.提供公开的获取唯一对象的静态方法
* 缺点:1. 线程安全性问题:在最基本的懒汉式实现中,当多个线程同时调用`getInstance()`方法时,可能会导致创建多个实例。这是
* 因为在并发环境下,多个线程可以同时通过判断实例是否为空来进入创建实例的逻辑,从而创建多个实例对象,破坏了单例的原则。
* 2. 性能问题:为了解决线程安全性问题,可以在`getInstance()`方法上添加`synchronized`关键字,实现同步控制,确保只
* 有一个线程能够创建实例。然而,使用`synchronized`会导致方法级别的锁定,使得多个线程在并发访问时需要排队等待,降低
* 了并发性能。
* 3. 实例创建的时机不可控:懒汉式是延迟加载的,只有在首次调用`getInstance()`方法时才会创建实例。这意味着如果在应用
* 程序启动时就需要预先创建实例,或者希望在程序启动阶段进行一些初始化操作,懒汉式无法满足这些需求。
* 4. 可能被反射攻击破坏单例:通过反射机制,可以调用私有的构造函数创建对象实例,从而破坏懒汉式的单例特性。为了解决这
* 个问题,可以在构造函数中添加逻辑,判断是否已经存在实例,并抛出异常阻止多次实例化。
* 使用场景:
* 在非高并发环境下,或者对性能要求不是非常高的情况下,懒汉式可以提供一种简单的延迟加载的单例实现。如果需要
* 解决懒汉式的线程安全性问题和性能问题,可以考虑其他实现方式,如静态内部类单例模式或枚举单例模式。
* @author: joywu
* @date:2021-06-08 22:24:15
*/
public class Singleton2 {
/**
* 1.私有构造方法
*/
private Singleton2() {}
/**
* 2.提供该类的静态变量,不是马上创建
*/
private static Singleton2 single = null;

/**
* 3.提供公开的获取唯一对象的静态方法 存在线程安全问题
*/
public static Singleton2 getInstance() {
//如果对象为空,则创建,否则直接返回
if (single == null) {
single = new Singleton2();
}
return single;
}
/**
* 在上面的方法中进行改进,解决线程不安全的问题
*/
public static synchronized Singleton2 getInstance1() {
//如果对象为空,则创建,否则直接返回
if (single == null) {
single = new Singleton2();
}
return single;
}
}
  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
55
56
package com.jw.cloud.modulepattern.singleton;

/**
* 双重检查锁单例模式:延迟加载、线程安全的单例模式实现方式
*
* 优点:
* 1. 延迟加载:双重检查锁单例模式实现了延迟加载,在首次调用`getInstance()`方法时才会创建实例。
* 这样可以避免在应用程序启动时就创建实例,提高了性能和资源利用率。
* 2. 线程安全:通过在关键代码段使用双重检查锁机制,即在首次检查实例是否为空之后再进行同步块的检查
* 和实例创建,确保了多线程环境下的线程安全性。只有第一个获取到锁的线程会创建实例,其他线程在获得锁
* 之后会发现实例已经被创建,避免了创建多个实例的问题。
* 3. 性能较好:相比于直接在方法上使用`synchronized`关键字进行同步的懒汉式,双重检查锁单例模
* 式在实例创建之后,后续的访问不需要进入同步块,避免了不必要的同步开销,提高了性能。
* 缺点:
*1. 可能的指令重排问题:在某些JVM和编译器的优化下,`instance = new Singleton()`这一行代码可
* 能会被重排序,导致一个线程在第一次检查实例是否为空时,得到一个非空但未完全初始化的实例。这种情况下,
* 其他线程在获得锁之后可能会使用到不完全初始化的实例,引发问题。为了解决这个问题,可以使用`volatile`
* 关键字修饰实例变量,确保可见性和禁止重排序。
* 2. 可能受到反射攻击:通过反射机制,可以调用私有的构造函数创建对象实例,从而破坏单例的特性。为了防止反
* 射攻击,可以在构造函数中添加逻辑,判断是否已经存在实例,并抛出异常阻止多次实例化。
* 说明:虽然双重检查锁单例模式在绝大多数情况下是安全的并且性能较好,但需要注意在特定环境下可能出现的指令重
* 排问题和反射攻击问题。如果对性能要求非常高且不考虑并发访问的情况,可以考虑使用其他实现方式,如枚举单例模式。
*
* @author: joywu
* @date:2021-06-08 22:26:32
*/
public class Singleton4 {
private static volatile Singleton4 singletonMode;
/**
* 构造函数修饰为private防止在其他地方创建该实例
*/
private Singleton4() {
}
/**
* 有的代码中会将同步锁synchronized写在方法中,例如:
* public static synchronized SingletonMode getInstance(){.....}
* 造成的弊端就是:多线程每次在调用getInstance()时都会产生一个同步,造成损耗。
* 相应的我们需要保持同步的代码块仅仅就是:
* singletonMode = new SingletonMode();
* 所以只要在该代码处添加同步锁就可以了
*/
public static Singleton4 getInstance() {
// 此处检测singletonMode == null,是为了防止当singletonMode已经初始化后, 还会继续调用同步锁,造成不必要的损耗
if (singletonMode == null) {
// 加锁目的,防止多线程同时进入造成对象多次实例化
synchronized (Singleton4.class) {
// 为了在null的情况下创建实例,防止不必要的损耗
if (singletonMode == null) {
singletonMode = new Singleton4();
}
}
}
// 返回实例对象
return singletonMode;
}
}
  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 com.jw.cloud.modulepattern.singleton;

/**
* 枚举单例模式
* 优点:
* 1. 线程安全:枚举单例模式天然具有线程安全性,保证只有一个实例。
* 2. 简洁明了:代码量少,易于理解。
* 3. 防止反射攻击和序列化问题:枚举实例是有限的,无法通过反射创建新实例,并且序列化和反序列化时会自动处理。
* 缺点:
* 1. 不支持延迟加载:枚举单例模式在类加载阶段就会创建实例,无法实现延迟加载。
* 2. 限制扩展性:由于枚举类型是固定的,无法继承其他类或实现接口,可能限制单例类的扩展性。
* @author JoyWu
*/

public enum Singleton5 {
/**
* 单例对象
*/
INSTANCE;
/**
* 公共方法,提供对外访问单例对象的方式
*/
public static Singleton5 getInstance() {
return INSTANCE;
}
}