单例模式
# 单例模式
- 构造器私有
- 自行创建实例
- 对外提供实例
饿汉式:类加载时初始化。 懒汉式:延迟初始化,需要考虑并发问题。
提示
私有化构造器并不保险。因为它抵御不了反射攻击。
# 常用创建方式
# 双重锁检查(Double Check Lock)
public class Singleton {
private static Singleton singleton;
private Singleton (){}
public static Singleton getSingleton() {
//如果单例已经存在,那么就不再需要进行同步操作,而是直接返回这个实例,如果没有创建,才会进入同步块
if (singleton == null) {
//同步块的目的与之前相同,目的是为了防止有两个调用同时进行时,导致生成多个实例,有了同步块,每次只能有一个线程调用能访问同步块内容,当第一个抢到锁的调用获取了实例之后,这个实例就会被创建,之后的所有调用都不会进入同步块,直接在第一重判断就返回了单例
synchronized (Singleton.class) {
//为了进入synchronized块后只有对象为null的情况下才创建实例,避免重复创建对象实例,而且synchronized块锁住的是类的Class对象,保证了在多线程环境下只有一个线程进入synchronized块
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}
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
少数情况下DCL还是会出问题,因为语句singleton = new Singleton();
分为三步:
- 在Java堆中分配内存空间
- 进行Singleton初始化
- 将singlten指向分配的内存空间
某些JVM会把这3个步骤进行指令重排序,变为以下顺序:
- 在java堆分配中Singleton对象的内存空间
- 把singleton指向1步骤中分配的内存空间
- 进行Singleton类的初始化过程.
如果在单线程环境是没有问题的,因为即使发生指令重排,在getSingtleton
方法返回的时候,singleton
指向的对象已经完成了初始化。但是在多线程环境中,当第一个线程先执行的1、3步骤,此时singtleton
已不为null,但还未初始化。当第二个线程进入getSingleton
,判断singleton==null
为false
直接return
,那么就拿到了一个还未初始化完毕的对象,这样就可能导致程序崩溃。
针对这种问题,可以使用volatile
来禁止指令重排序
public class Singleton {
//加上Volatile关键字修饰
private volatile static Singleton singleton;
private Singleton (){}
public static Singleton getSingleton() {
//如果单例已经存在,那么就不再需要进行同步操作,而是直接返回这个实例,如果没有创建,才会进入同步块
if (singleton == null) {
//同步块的目的与之前相同,目的是为了防止有两个调用同时进行时,导致生成多个实例,有了同步块,每次只能有一个线程调用能访问同步块内容,当第一个抢到锁的调用获取了实例之后,这个实例就会被创建,之后的所有调用都不会进入同步块,直接在第一重判断就返回了单例
synchronized (Singleton.class) {
//为了进入synchronized块后只有对象为null的情况下才创建实例,避免重复创建对象实例,而且synchronized块锁住的是类的Class对象,保证了在多线程环境下只有一个线程进入synchronized块
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}
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 Singleton {
private static Singleton singleton;
private Singleton (){}
public static Singleton getSingleton() {
//1、增加一个局部变量,同为Singleton类型
Singleton singletonVar = null;
//如果单例已经存在,那么就不再需要进行同步操作,而是直接返回这个实例,如果没有创建,才会进入同步块
if (singleton == null) {
//同步块的目的与之前相同,目的是为了防止有两个调用同时进行时,导致生成多个实例,有了同步块,每次只能有一个线程调用能访问同步块内容,当第一个抢到锁的调用获取了实例之后,这个实例就会被创建,之后的所有调用都不会进入同步块,直接在第一重判断就返回了单例
synchronized (Singleton.class) {
//为了进入synchronized块后只有对象为null的情况下才创建实例,避免重复创建对象实例,而且synchronized块锁住的是类的Class对象,保证了在多线程环境下只有一个线程进入synchronized块
if (singleton == null) {
//2、执行实例时,先实例化这个局部变量
singletonVar = new Singleton();
//3、待局部变量实例化完毕后,才把这个实例赋值给要返回的静态变量singleton
singleton = singletonVar;
}
}
}
return singleton;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
需要配合volatile内存可见性
# 静态内部类
利用类加载机制,类只有在被用到时才会被加载,静态属性在类加载的时候会被创建。
public class Singleton {
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}
private Singleton (){}
public static final Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
}
1
2
3
4
5
6
7
8
9
2
3
4
5
6
7
8
9
# 枚举方式
这种实现方式还没有被广泛采用,但这是实现单例模式的最佳方法。它更简洁,自动支持序列化机制,绝对防止多次实例化。 这种方式是 Effective Java 作者 Josh Bloch 提倡的方式,它不仅能避免多线程同步问题,而且还自动支持序列化机制,防止反序列化重新创建新的对象,绝对防止多次实例化。不过,由于 JDK1.5 之后才加入 enum 特性,用这种方式写不免让人感觉生疏,在实际工作中,也很少用。 不能通过 reflection attack 来调用私有构造方法。
public class Singleton {
private Singleton(){
}
public static enum SingletonEnum {
SINGLETON;
private Singleton instance = null;
private SingletonEnum(){
instance = new Singleton();
}
public Singleton getInstance(){
return instance;
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
2
3
4
5
6
7
8
9
10
11
12
13
14
编辑 (opens new window)
上次更新: 2023/12/28, 14:40:19