单例模式指仅被实例化一次的类。这个设计模式目的是想在整个系统中只能出现这个类的一个实例。
三种实现方式:饿汉、懒汉、枚举
在Java1.5发行版之前,实现Singleton有两种方法。这两种方法都要把构造器保持为私有的,并导出公有的静态成员,以便允许客户端访问该类的唯一实例。
懒汉式
Version 1.0
懒汉式最简单的实现,我们称之为1.0版本
懒汉式实现单例方法与饿汉之间的区别是:实例只有在被调用的时候才会初始化。1
2
3
4
5
6
7
8
9
10
11// version1.1
public class Singleton {
private static Singleton singleton = null;
private Singleton() {}
public static Singleton getInstance() {
if (singleton == null) {
singleton = new Singleton();
}
return singleton;
}
}
1.0版本存在比较严重的问题,因为这个singleton
是静态的全局变量,所以在多线程下,如果多个线程同时调用getInstance()方法的话,可能会有多个进程通过(singleton == null)
检查,从而创建多个实例。熟悉多线程的你一定会说——“我们需要线程互斥或同步”,于是,就有了1.1版本
Version 1.1
en,1.1版本使用Synchronized关键字,看起来没有问题了吧?
NO!!!如果有多个线程同时通过(singleton == null)
检查,还是一样会创建多个实例,只是变成了串行创建而已。1
2
3
4
5
6
7
8
9
10
11
12
13//Version 1.1
public class Singleton {
private static Singleton singleton = null;
private Singleton() {}
public static Singleton getInstance() {
if (singleton== null) {
synchronized (Singleton.class) {
singleton= new Singleton();
}
}
return singleton;
}
}
于是我们继续升级,有了版本1.2.
Version 1.2
经过修改之后的1.2版本确实不会出现上面的问题。
但是啊,我们每次调用getInstance()方法要进行同步,创建动作只有一次,但是读取操作也被同步了,效率低到令人发指。改!!!1
2
3
4
5
6
7
8
9
10
11
12
13// version1.2
public class Singleton {
private static Singleton singleton = null;
private Singleton(){}
public static Singleton getInstance() {
synchronized (Singleton.class) {
if (singleton== null) {
singleton= new Singleton();
}
}
return singleton;
}
}
Version 1.3
还得改,嗯,看来,在线程同步前还得加一个(singleton== null)的条件判断,如果对象已经创建了,那么就不需要线程的同步了。OK,下面是1.3版的Singleton。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15// version 1.3
public class Singleton {
private static Singleton singleton = null;
private Singleton(){}
public static Singleton getInstance() {
if (singleton== null) {
synchronized (Singleton.class) {
if (singleton== null) {
singleton= new Singleton();
}
}
}
return singleton;
}
}
感觉代码开始变得有点罗嗦和复杂了,不过,这可能是最不错的一个版本了,这个版本又叫“双重检查”Double-Check。下面是说明:
- 第一个条件是说,如果实例创建了,那就不需要同步了,直接返回就好了。
- 不然,我们就开始同步线程。
- 第二个条件是说,如果被同步的线程中,有一个线程创建了对象,那么别的线程就不用再创建了。
这个版本已经相当漂亮了,但是,如果你认为这个版本大功告成,too young.
主要在于singleton = new Singleton()
这句,这并非是一个原子操作,事实上在 JVM 中这句话大概做了下面 3 件事情。
- 给 singleton 分配内存
- 调用 Singleton 的构造函数来初始化成员变量,形成实例
- 将singleton对象指向分配的内存空间(执行完这步 singleton才是非 null 了)
但是在 JVM 的即时编译器中存在指令重排序的优化。也就是说上面的第二步和第三步的顺序是不能保证的,最终的执行顺序可能是 1-2-3 也可能是 1-3-2。如果是后者,则在 3 执行完毕、2 未执行之前,被线程二抢占了,这时 instance 已经是非 null 了(但却没有初始化),所以线程二会直接返回 instance,然后使用,然后顺理成章地报错。
对此,我们只需要把singleton声明成 volatile 就可以了。下面是1.4版:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15// version 1.4
public class Singleton {
private volatile static Singleton singleton = null;
private Singleton(){}
public static Singleton getInstance() {
if (singleton== null) {
synchronized (Singleton.class) {
if (singleton== null) {
singleton= new Singleton();
}
}
}
return singleton;
}
}
使用 volatile 有两个功用:
- 这个变量不会在多个线程中存在复本,直接从内存读取。
- 这个关键字会禁止指令重排序优化。也就是说,在 volatile 变量的赋值操作后面会有一个内存屏障(生成的汇编代码上),读操作不会被重排序到内存屏障之前。
但是,这个事情仅在Java 1.5版后有用,1.5版之前用这个变量也有问题,因为老版本的Java的内存模型是有缺陷的。
饿汉式
1 | public class HugrySingleton { |
这种方法最大的问题是——当这个类被加载的时候,new Singleton()语句会被执行,无论getINSTANCE()是否被调用都会初始化这个类。于是,这个可能会与我们想要的行为不一样,比如,我的类的构造函数中,有一些事可能需要依赖于别的类干的一些事(比如某个配置文件,或是某个被其它类创建的资源),我们希望他能在我第一次getInstance()时才被真正的创建。这样,我们可以控制真正的类创建的时刻,而不是把类的创建委托给了类装载器。
好吧,我们还得绕一下:
下面的这个1.6版是老版《Effective Java》中推荐的方式。1
2
3
4
5
6
7
8
9
10// version 1.6
public class Singleton {
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}
private Singleton (){}
public static final Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
}
上面这种方式,仍然使用JVM本身机制(类只会被加载一次,在加载类时初始化了静态变量INSTANCE)保证了线程安全问题;由于 SingletonHolder 是私有的,除了 getInstance() 之外没有办法访问它,因此它只有在getInstance()被调用时才会真正创建;同时读取实例的时候不会进行同步,没有性能缺陷;也不依赖 JDK 版本。
枚举单例
从Java 1.5发行版本起,实现Singleton还有第三种方法。只需编写一个包含单个元素的枚举类型:1
2
3public enum SingletonEnum {
SINGLETON_ENUM;
}
通过SingletonEnum.SINGLETON_ENUM来访问,比使用getInstance()方法简单多了。
默认枚举实例的创建是线程安全的,所以不需要担心线程安全的问题。但是在枚举中的其他任何方法的线程安全由程序员自己负责。还有防止上面的通过反射机制调用私用构造器。
这个版本基本上消除了绝大多数的问题。代码也非常简单,实在无法不用。这也是新版的《Effective Java》中推荐的模式。
克隆、序列化、反射——单例模式防御
使用饿汉式和懒汉式方法创建的单例,能够解决同步问题,然而很遗憾,这样并不能真正地实现单例,我有可能通过克隆、序列化、反射机制,来击破单例的模式。
克隆,当你的单例类需要继承Cloneable接口时,就可以通过clone方法获取一个新的对象,那么单例防御失败。
同理,序列化也可以,或者用反射,也可以。1
2
3//获取构造函数
Constructor constructor = SingleTon.class.getDeclaredConstructor();
constructor.setAccessible(true);
这样获取到了构造函数,设置可以访问,然后直接newInstance,就可以获取一个新的实例了。
真正的单例,应当是可以抵御上述攻击的——
一、抵御Clone攻击
测试的单例模式实现了Cloneable接口,重写克隆方法:1
2
3
4
5
6
7
8
9/**
* 防止克隆攻击
* @return
* @throws CloneNotSupportedException
*/
protected Object clone() throws CloneNotSupportedException {
return getInstance();
}
So Easy
测试代码: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// version 1.4
public class Singleton implements Cloneable {
private volatile static Singleton singleton = null;
private Singleton(){}
public static Singleton getInstance() {
if (singleton== null) {
synchronized (Singleton.class) {
if (singleton== null) {
singleton= new Singleton();
}
}
}
return singleton;
}
//如果注释此方法,下面的输出为false
protected Object clone() throws CloneNotSupportedException {
return getInstance();
}
public static void main(String[] args) throws CloneNotSupportedException {
Singleton instance = getInstance();
Singleton singleton1 = (Singleton) instance.clone();
System.out.println(instance==singleton1);//true
}
}
二、抵御序列化攻击
单例类实现Serializable接口,然后重写一个方法:1
2
3
4
5
6
7/**
* 防止序列化攻击
* @return
*/
private Object readResolve() {
return getInstance();
}
So Easy,比攻击的代码简单多了: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
38public class Singleton implements Serializable {
private volatile static Singleton singleton = null;
private Singleton(){}
public static Singleton getInstance() {
if (singleton== null) {
synchronized (Singleton.class) {
if (singleton== null) {
singleton= new Singleton();
}
}
}
return singleton;
}
//如果注释此方法,返回结果为false
private Object readResolve(){
return getInstance();
}
public static void main(String[] args) throws CloneNotSupportedException, IOException, ClassNotFoundException {
Singleton singleton1 = getInstance();
System.out.println("序列化攻击被阻止了吗?");
File file = new File("serializable.txt");
//序列化
FileOutputStream fos = new FileOutputStream(file);
ObjectOutputStream oos = new ObjectOutputStream(fos);
oos.writeObject(singleton1);
oos.flush();
oos.close();
fos.close();
//反序列化
FileInputStream fis = new FileInputStream(file);
ObjectInputStream ois = new ObjectInputStream(fis);
Singleton singleTon3 = (Singleton) ois.readObject();
ois.close();
fis.close();
System.out.println(singleton1 == singleTon3); // true
}
}
三、抵御反射攻击
这里的思路就是加一个flag,判断构造函数是否第一次被调用。1
2
3
4
5
6
7
8
9private static boolean flag = true;
private SingleTon() {
if (flag){
flag = false;
//code
}else {
throw new RuntimeException("对象已存在");
}
}
注意,这里的flag必须是private的,而且不能有getter setter函数。
反射攻击的思路是,获取构造函数,然后获取到flag这个域,之后把这个flag置true,然后用构造函数新建。这里要求flag必须有getter setter,不然无法执行,这就是我们防御的思路。
代码如下: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
62package cap.ljf.pattern.singleton;
import java.beans.IntrospectionException;
import java.beans.PropertyDescriptor;
import java.io.IOException;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
/**
* @author jifang.liu created on 2018/8/13 21:07
*/
// version 1.4
public class Singleton {
private volatile static Singleton singleton = null;
private static boolean flag = true;
private Singleton(){
if (flag){
flag = false;
//code
}else {
throw new RuntimeException("对象已存在");
}
}
public static boolean isFlag() {
return flag;
}
public static void setFlag(boolean flag) {
Singleton.flag = flag;
}
public static Singleton getInstance() {
if (singleton== null) {
synchronized (Singleton.class) {
if (singleton== null) {
singleton= new Singleton();
}
}
}
return singleton;
}
public static void main(String[] args) throws NoSuchMethodException, IntrospectionException, InvocationTargetException, IllegalAccessException, InstantiationException {
Singleton singleton1 = getInstance();
System.out.println("反射攻击被阻止了吗");
//获取构造函数
Constructor constructor = Singleton.class.getDeclaredConstructor();
constructor.setAccessible(true);
//要求该域必须有getter,setter,否则java.beans.IntrospectionException: Method not found: isFlag
PropertyDescriptor descriptor = new PropertyDescriptor("flag", Singleton.class);
//每次新建一个实例前,将flag设置为true
descriptor.getWriteMethod().invoke(Singleton.class, true);
Singleton singleTon4 = (Singleton) constructor.newInstance();
System.out.println(singleton1 == singleTon4);//false
}
}
至此,我们完成的单例模式的代码如下: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
54public class SingleTon implements Serializable, Cloneable{
private static final long serialVersionUID = 1L;
private static volatile SingleTon singleTon;
private static boolean flag = true;
private SingleTon() {
if (flag){
flag = false;
//code
}else {
throw new RuntimeException("对象已存在");
}
}
public static SingleTon getInstance(){
if (singleTon == null){
synchronized (SingleTon.class){
if (singleTon == null){
singleTon = new SingleTon();
}
}
}
return singleTon;
}
/**
* 防止克隆攻击
* @return
* @throws CloneNotSupportedException
*/
protected Object clone() throws CloneNotSupportedException {
return getInstance();
}
// public static boolean getFlag() {
// return flag;
// }
//
// public static void setFlag(boolean flag) {
// SingleTon.flag = flag;
// }
/**
* 防止序列化攻击
* @return
*/
private Object readResolve() {
return getInstance();
}
}
真是太麻烦了,有没有简单的方法呢?
那就是使用枚举:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17public enum SingleEnum implements Cloneable, Serializable{
INSTANCE;
private String name;
public SingleEnum getInstance(){
System.out.println(this == INSTANCE); // true
return INSTANCE;
}
public static void main(String[] args) {
SingleEnum singleEnum = SingleEnum.INSTANCE;
singleEnum.name = "枚举";
System.out.println(singleEnum.name); // 枚举
System.out.println(singleEnum.getInstance()); // true INSTANCE
}
}
一个枚举,就算实现双接口,也是无论如何都无法被破坏的。
攻击代码如下: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
43System.out.println("枚举实验");
SingleEnum singleEnum1 = SingleEnum.INSTANCE;
System.out.println("直接获取");
SingleEnum singleEnum2 = SingleEnum.INSTANCE;
System.out.println(singleEnum1 == singleEnum2); // true
System.out.println("枚举克隆攻击通过了吗?");
System.out.println("枚举无法克隆");
System.out.println("枚举序列化攻击通过了吗?");
File enumTxt = new File("enumTest.txt");
//序列化
FileOutputStream fosEnum = new FileOutputStream(enumTxt);
ObjectOutputStream oosEnum = new ObjectOutputStream(fosEnum);
oosEnum.writeObject(singleEnum1);
oosEnum.flush();
oosEnum.close();
fosEnum.close();
//反序列化
FileInputStream fisEnum = new FileInputStream(enumTxt);
ObjectInputStream oisEnum = new ObjectInputStream(fisEnum);
SingleEnum singleEnum3 = (SingleEnum) oisEnum.readObject();
fisEnum.close();
oisEnum.close();
System.out.println(singleEnum1 == singleEnum3); // true
System.out.println("枚举反射攻击通过了吗?");
Class enumClass = singleEnum1.getClass();
/*
java.lang.InstantiationException
Caused by: java.lang.NoSuchMethodException: SingleEnum.<init>()
*/
//stop run
SingleEnum singleEnum5 = (SingleEnum) enumClass.newInstance();
System.out.println(singleEnum1 == singleEnum5);
//stop run
Constructor enumConstructor = SingleEnum.class.getConstructor(); // java.lang.NoSuchMethodException
enumConstructor.setAccessible(true);
SingleEnum singleEnum4 = (SingleEnum) enumConstructor.newInstance();
System.out.println(singleEnum1 == singleEnum4);
直接获取:true
克隆:枚举无法克隆,没有这样的方法。
反射:没有构造函数,会抛出异常。就算你在枚举里加了构造函数,也是一样的。
完美。
参考文献:
- 深入浅出单实例Singleton设计模式,haoel
- 克隆、序列化、反射——单例模式防御心得,葛尧
- 《Effective Java第二版》第三条,Joshua Bloch