创建型-单例模式
# 什么是单例模式?
- 一个应用中,一个类有且仅有一个实例的情况。
- 在 Java 中就是每个 JVM,一个实例。
- 私有化构造器,返回创建好的对象(保持只有一个对象)
# 为什么要使用单例?
- 在内存中只有一个实例对象,节省内存空间.避免重复的创建和销毁对象。
- 可以提高性能,避免对多重资源的重复占用,可以全局进行访问。
- 使用单例解决资源访问冲突的问题。
# 如何编写单例?
# 饿汉模式(简单)
- 优点
- 在类加载的时已初始化好,是线程安全的
- 缺点
- 对象大时,占用系统资源较多
- 如果实例占用资源多或初始化耗时长,影响性能
- 适用
- 对象不大,占用系统资源较少
public class Singleton01 {
private static final Singleton01 INSTANCE = new Singleton01();
private Singleton01() {
}
public static Singleton01 getInstance() {
return INSTANCE;
}
}
1
2
3
4
5
6
7
8
9
10
2
3
4
5
6
7
8
9
10
# 饿汉模式(静态块)
public class Singleton02 {
private static final Singleton02 INSTANCE;
private Singleton02() {
}
static {
try {
INSTANCE = new Singleton02();
} catch (Exception e) {
throw new RuntimeException(" error! ");
}
}
public static Singleton02 getInstance() {
return INSTANCE;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 懒汉模式(简单,非安全)
- 优点
- 支持延迟加载
- 缺点
- 非线程安全
public class Singleton03 {
private static Singleton03 INSTANCE;
private Singleton03() {
}
public static Singleton03 getINSTANCE() {
if (INSTANCE == null) {
INSTANCE = new Singleton03();
}
return INSTANCE;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
2
3
4
5
6
7
8
9
10
11
12
13
# 懒汉模式(简单,同步)
public class Singleton04 {
private static Singleton04 INSTANCE;
private Singleton04() {
}
private static synchronized Singleton04 getInstance() {
if (INSTANCE == null) {
INSTANCE = new Singleton04();
}
return INSTANCE;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
2
3
4
5
6
7
8
9
10
11
12
13
# 懒汉模式(双重检测)
public class Singleton05 {
private static volatile Singleton05 INSTANCE;
private Singleton05() {
}
public static Singleton05 getInstance() {
if (INSTANCE == null) {
synchronized (Singleton05.class) {
if (INSTANCE == null) {
INSTANCE = new Singleton05();
}
}
}
return INSTANCE;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# Bill Pugh 单例(静态内部类)
- 优点
- 即保证了线程安全,又能做到延迟加载
public class Singleton06 implements Serializable {
private Singleton06() {
}
private static class LazyHolder {
private final static Singleton06 INSTANCE = new Singleton06();
}
public static Singleton06 getInstance() {
return LazyHolder.INSTANCE;
}
// 确保反序列化单例
protected Object readResolve() {
return getInstance();
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 枚举单例
- 基于枚举类型的单例实现,保证了实例创建的线程安全性和实例的唯一性。
public enum Singleton07 {
INSTANCE;
private AtomicLong id = new AtomicLong(0);
public long getId() {
return id.incrementAndGet();
}
}
class TestEnumSingleton{
public static void main(String[] args) {
System.out.println(Singleton07.INSTANCE.getId());
System.out.println(Singleton07.INSTANCE.getId());
System.out.println(Singleton07.INSTANCE.getId());
System.out.println(Singleton07.INSTANCE.getId());
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 单例存在哪些问题?
- 单例对 OOP 特性的支持不友好
- 单例模式对继承、多态特性的支持不太友好,从理论上讲可以实现继承和多态,但是这样的设计会导致代码的可读性变差。
- 一般选用单例模式,相当于损失了可以应对未来需求变化的扩展性。
- 单例会隐藏类之间的依赖关系
- 通过构造函数、参数传递等方式声明的类之间的依赖关系,我们通过查看函数的定义就能容易识别出来。
- 单例类不需要显示创建、不需要依赖参数传递,在函数中的直接调用就可以,如果代码复杂,这种依赖关系不能很方便了解。
- 单例对代码的扩展性不友好
- 虽然生成全局一个对象的实例使用单例模式很棒,但是如果未来业务有所变化,系统需要两个功能类似的类,在整个 JVM 上不好扩展。
- 单例对代码的可测试性不友好
- 虽然我们可以使用 Mock 可以对单例进行测试,但是如果单例类持有成员变量,那它实际会被所有代码共享,如果这个全局变量是一个可变变量,那么在并发测试的过程中可能会有不同的结果。
- 单例不支持有参数的构造函数
- 因为我们知道,如果我们传递了对函数构造有关的参数,那么可能会导致,每次调用使用不同参数,得到不同的对象,违背的单例的目的。
- 可通过反射创建对象
- 解决:在第二次创建对象的时候抛出异常
- 在序列化和反序列话时不是同一对象
- 解决:添加单例类添加
readResolve()
方法
- 解决:添加单例类添加
# 单例模式与静态类的区别?
- Static 类有更好的访问效率。
- 单例比静态类更容易测试,单例模式更容易 Mock,可以使用 JUnit 单元测试。
- 如果需要维护有状态的数据,使用单例比静态类更好。
- 单例模式支持懒加载,而静态类不支持。
- 对于一些依赖注入框架,能够很好的管理单例对象。
- 单例类相比静态类更加面向对象,可以使用继承和多态继承一个基类,实现一个接口,提供不同的功能。
- Java 源码中的一些实现
- java.lang.Runtime,该类使用了单例模式。
- java.lang.Math,该类使用 static 来实现的。
# 如何理解单例模式中的唯一性?
- 在一个进程中,一个类只允许撞见唯一一个对象,那么这个类就是一个单例类。
- 进程之间是不共享地址空间的,如果我们在一个进程中创建另外一个进程,操作系统会给新进程分配新的地址空间,并且将老进程地址空间的所有内容拷贝一份到新进程的地址空间中,这些内容包括代码、数据(临时变量、对象)。
# 如何实现线程唯一的单例?
- “进程唯一”指的是进程内唯一,进程间不唯一。
- “线程唯一”指的是线程内唯一,线程间可以不唯一。
- 一般对应的实现思路,使用一个 Map,可以是线程ID,value 是对象。
- Java 语言本事提供了 ThreadLocal 工具类,可以轻松实现线程唯一单例。
# 如何实现集群环境下的单例?
- 集群环境的下的单例就是指不同进程间要保证唯一。
- 一般的实现思路就是需要使用一个公共共享区域存储序列化文件。
- 具体操作是我们把这个单例对象序列化存储到外部共享存储区,进程在使用这个单例对象的时候,需要先从外部共享存储区中将它读取到内存中,并反序列化成对象,然后再使用,使用完成之后还需要存储回外部共享存储区。
- 为了保证任何时刻,在进程间都只有一份对象存在,一个进程在获取到对象之后,需要对对象加锁,避免其他进程再将其获取。在进程使用完这个对象之后,还需要显式地将对象从内存中删除,并且释放对对象的加锁。
# 如何实现一个多例模式?
- 一个类可以创建多个对象,并且创建个数有限,我们称之为多例模式。
- 一般实现思路也是使用一个 Map ,将对应的不同实例放到其中。
public class BackendServer {
private long serverNo;
private String serverAddress;
private static final int SERVER_COUNT = 3;
private static final Map serverInstances = new HashMap<>();
static {
serverInstances.put(1L, new BackendServer(1L, "192.134.22.138:8080"));
serverInstances.put(2L, new BackendServer(2L, "192.134.22.139:8080"));
serverInstances.put(3L, new BackendServer(3L, "192.134.22.140:8080"));
}
private BackendServer(long serverNo, String serverAddress) {
this.serverNo = serverNo;
this.serverAddress = serverAddress;
}
public BackendServer getInstance(long serverNo) {
return serverInstances.get(serverNo);
}
public BackendServer getRandomInstance() {
Random r = new Random();
int no = r.nextInt(SERVER_COUNT) + 1;
return serverInstances.get(no);
}
}
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
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
上次更新: 2020/09/08, 15:09:00