Sorry, your browser cannot access this site
This page requires browser support (enable) JavaScript
Learn more >

单例模式


​ 不能滥用static。

​ 正常的设计模式上不能随便使用static变量,应该先懂得如何编写足以证明“赋值结果冲突、混乱”的测试用例,然后再使用static变量。


单例模式

单例模式意味着该类只会有一个实例,而且自行实例化并向整个系统提供该实例。

五种实现方式:懒汉、饿汉、双重锁、静态内部类、Enum

1. 懒汉单例

懒汉单例,在需要用到该实例的时候才会实例化,能够延迟实例化,节约资源。如下简单的懒汉单例是非线程安全的。因为在进入singleton == null之后,有可能被其他线程中断,从而实例两个对象。

1
2
3
4
5
6
7
8
9
10
11
12
class LazySingleton {
private static LazySingleton singleton;

private LazySingleton() {
}
public static LazySingleton getLazySingleton() {
if (singleton == null) {
singleton = new LazySingleton();
}
return singleton;
}
}

懒汉单例满足线程安全的最简单的方式就是在getSingleton方法加上synchronized修饰,使得该方法同步。但是该方式会对LazySingleton加类锁,所得粒度比较粗,并发性降低。

2. 饿汉单例

饿汉单例在类加载、加载静态数据的时候就会实例。这样虽然是线程安全的但是享受不到延迟实例化带来的好处(节约资源)。

1
2
3
4
5
6
7
8
class EagerSingletonCocurrency {
private static EagerSingletonCocurrency singleton = new EagerSingletonCocurrency();
private EagerSingletonCocurrency() {
}
public static EagerSingletonCocurrency getEagerSingletonCocurrency() {
return singleton;
}
}

3. 双重锁机制

双重锁机使得线程安全,同时也能享受延迟实例化的好处。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class DoubleLockSingleton{
//volatile 关键字确保:当singleton变量被初始化成DoubleLockSingleton实例时,多个线程正确地处理singleton变量。
private static volatile DoubleLockSingleton singleton;
private DoubleLockSingleton(){}
public static getDoubleLockSingleton(){
//防止在singleton != null的时候也会加锁。
if(singleton == null){
synchronized(DoubleLockSingleton.class){
//当==null之后进入同步代码块,再次判断时候为null。
// 因为在进入同步之前有可能有线程中断该位置,已经实例化了单例。
if(singleton ==null){
singleton = new DoubleLockSingleton();
}
}
}
}
}

该段出处:CS-Notes博客的[单例(Singleton)](https://cyc2018.github.io/CS-Notes/#/notes/设计模式 - 单例?id=Ⅳ-双重校验锁-线程安全)

singleton采用 volatile 关键字修饰也是很有必要的, singleton= new DoubleLockSingleton(); 这段代码其实是分为三步执行:

  1. 为 singleton分配内存空间
  2. 初始化 singleton
  3. 将 singleton指向分配的内存地址

但是由于 JVM 具有指令重排的特性,执行顺序有可能变成 1>3>2。指令重排在单线程环境下不会出现问题,但是在多线程环境下会导致一个线程获得还没有初始化的实例。例如,线程 T1 执行了 1 和 3,此时 T2 调用 getDoubleLockSingleton() 后发现 singleton不为空,因此返回 singleton,但此时 singleton还未被初始化。

使用 volatile 可以禁止 JVM 的指令重排,保证在多线程环境下也能正常运行。

4. 静态内部类

当StaticSingleton被加载的时候,静态内部类还不会被加载。只有当执行getStaticSingleton()方法的时候才会被加载。同时JVM保证线程安全和只被实例化一次。

1
2
3
4
5
6
7
8
9
10
11
class StaticSingleton{
private StaticSingleton(){}
static class StaticSingletonHolder{
// 静态内部类,
private static StaticSingleton singleton = new StaticSingleton();
}
public StaticSingleton getStaticSingleton(){
return StaticSingletonHolder.singleton;
}

}

当 StaticSingleton类被加载时,静态内部类 StaticSingletonHolder没有被加载进内存。只有当调用 getStaticSingleton() 方法从而触发 StaticSingletonHolder.INSTANCE 时 StaticSingletonHolder才会被加载,此时初始化 singleton实例,并且 JVM 能确保 INSTANCE 只被实例化一次。

这种方式不仅具有延迟初始化的好处,而且由 JVM 提供了对线程安全的支持。

5. 枚举实现

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
/**
* 枚举单例
*
* @author: Maple Chan
* @date: 2020-07-31 22:27:14
* @version: 0.0.1
*/
public enum EnumSingleton {
INSTANCE;

private User user;
private String notice;

public void doSomething() {
System.out.println("Do something!");
System.out.println(notice);
}
public User getUser() {
return user;
}
/**
* 枚举类被初始化时调用构造函数.每个构造函数将以成员声明顺序被调用, 不管实际引用和使用哪些成员.
*/
private EnumSingleton() {
user = new User();
notice = "Life is fantastic!";
}

class User {
private String name;
private User(){
name = "default name";
}
public String getUserName() {
return name;
}
}

public static void main(String[] args) {
EnumSingleton.INSTANCE.doSomething();
System.out.println(EnumSingleton.INSTANCE.getUser().getUserName());
}

}

/*
输出:
Do something!
Life is fantastic!
default name
*/

单例的枚举实现在Effective Java一书中提到。因为其功能完善,使用简洁,无偿地提供了序列化机制,在面对复杂的序列化或者反射攻击时任然可以绝对防止多次实例化等优点,被作者所推崇。

评论