0%

单例

一、含义

“单例”的书面含义是:在一个Java进程中,某个给定类的实例只允许存在一个。

二、实现方案

实现“单例”方案须满足两个核心目标:

  • 的确是“单例”
  • 线程安全

常见的单例实现方案有6种,具体可细分为3类:静态初始化锁机制,内置锁/高级锁,枚举。

接下来基于以下几个维度对上述6种单例实现方案进行评价:

  • 实现可读性,用5分制来进行评价
  • 性能
  • 是采用“懒汉模式(在获取单例实例时才生成)”还是“饿汉模式(在获取单例实例前已生成)”

2.1、静态初始化锁机制

利用静态初始化锁机制来获得“确保单例”和“线程安全”。
1、方案1

1
2
3
4
5
6
7
8
9
10
11
12
13
package single;

public class Singleton1 {

private static Singleton1 instance = new Singleton1();

private Singleton1() {
}

public static Singleton1 getInstance() {
return instance;
}
}
实现可读性 性能 懒汉模式/饿汉模式
5 饿汉模式

2、方案2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package single;

public class Singleton2 {

private static Singleton2 instance;

static {
instance = new Singleton2();
}

private Singleton2() {
}

public static Singleton2 getInstance() {
return instance;
}
}

一个简单变种。

实现可读性 性能 懒汉模式/饿汉模式
5 饿汉模式

3、方案3

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package single;

public class Singleton3 {

private Singleton3() {
}

public static Singleton3 getInstance() {
return SingletonHolder.INSTANCE;
}

private static class SingletonHolder {
private static final Singleton3 INSTANCE = new Singleton3();
}
}

采用懒汉模式,代价是损失一点可读性。

实现可读性 性能 懒汉模式/饿汉模式
4 懒汉模式

2.2、内置锁/高级锁

利用“内置锁(即synchronized锁)”或者高级锁来获得“线程安全”,利用条件判断获得“确保单例”。
4、方案4

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package single;

public class Singleton4 {

private static Singleton4 instance;

private Singleton4() {
}

public static synchronized Singleton4 getInstance() {
if (instance == null) {
instance = new Singleton4();
}

return instance;
}
}

每次获取单例都需要申请锁和释放锁,大部分情况是不必要的,性能较差。

实现可读性 性能 懒汉模式/饿汉模式
5 懒汉模式

5、方案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
package single;

public class Singleton5 {

private static Singleton5 instance;

private static volatile boolean init = false;

private Singleton5() {
}

public static synchronized Singleton5 getInstance() {
if (!init) {
synchronized (Singleton5.class) {
if (!init) {
instance = new Singleton5();

init = true;
}
}
}

return instance;
}
}

经典的双重检查锁定方式,“init”变量需要用volatile修饰的原因在于:否则,由于重排序/狭义可见性,外层的if (!init)语句返回false时,“instance”变量可能还未完成初始化。

实现可读性 性能 懒汉模式/饿汉模式
5 懒汉模式

在以下简单变种中,“instance”变量仍然需要用volatile修饰的原因在于:否则,由于重排序的存在,外层的if (instance == null)返回false时,“instance”变量可能还未完成初始化,只不过是由于重排序先返回了变量引用而已。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package single;

public class Singleton55 {

private static volatile Singleton55 instance;


private Singleton55() {
}

public static synchronized Singleton55 getInstance() {
if (instance == null) {
synchronized (Singleton55.class) {
if (instance == null) {
instance = new Singleton55();
}
}
}

return instance;
}
}

2.3、枚举

利用枚举自身的语法特性,来获得“确保单例”和“线程安全”。
6、方案6

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package single;

public enum Singleton6 {
SINGLE;

char c;

double d;

Singleton6() {
init();
}

public static Singleton6 getInstance() {
return SINGLE;
}

private void init() {
c = 'a';
d = 20D;
}
}
实现可读性 性能 懒汉模式/饿汉模式
3 饿汉模式

三、反单例攻击

“反单例攻击”含义:通过某种手段,在“单例”之外创建生成新实例。

常见的反单例攻击手段有:

  • 使用new + 构造方法创建新实例
  • 使用克隆创建新实例
  • 使用反射创建新实例
  • 使用序列化-反序列化创建新实例
  • 使用多类加载器加载创建新实例

3.1、new + 构造方法

构造方法暴露给外部,使得外部可直接使用new + 构造方法的形式创建新实例。

实验代码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
57
58
59
60
61
62
63
64
65
66
67
68
enum C {
SINGLE;

char c;

double d;

C() {
init();
}

public static C getInstance() {
return SINGLE;
}

private void init() {
c = 'a';
d = 20D;
}
}

class A {

private static A instance = new A();

public A() {
}

public static A getInstance() {
return instance;
}
}

class B {

private static B instance;

private static volatile boolean init = false;

public B() {
}

public static synchronized B getInstance() {
if (!init) {
synchronized (B.class) {
if (!init) {
instance = new B();

init = true;
}
}
}

return instance;
}
}

public class NewConstructorAttack {
public static void main(String[] args) {

System.out.println(A.getInstance() == new A());

System.out.println(B.getInstance() == new B());

//不被允许
//new C();
}
}

结论

  • 由于枚举类不支持使用new + 构造方法的形式创建新实例,因此方案6天然防御该形式攻击
  • 方案1-5为防御new + 构造方法攻击,可将构造方法设为由private修饰

3.2、克隆

类继承java.lang.Cloneable接口,并覆盖实现clone()方法,调用clone()方法可以创建生成一个新的实例。

实验代码2:

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
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
enum F implements Cloneable {
SINGLE;

char c;

double d;

F() {
init();
}

public static F getInstance() {
return SINGLE;
}

private void init() {
c = 'a';
d = 20D;
}

// 不允许覆盖实现Object类的clone()方法
// @Override
// public Object clone() {
//
// }
}

class D implements Cloneable {

private static D instance = new D();

private D() {
}

public static D getInstance() {
return instance;
}

@Override
public Object clone() throws CloneNotSupportedException {
return super.clone();
}
}

class E implements Cloneable {

private static E instance;

private static volatile boolean init = false;

private E() {
}

public static synchronized E getInstance() {
if (!init) {
synchronized (E.class) {
if (!init) {
instance = new E();

init = true;
}
}
}

return instance;
}

@Override
public Object clone() throws CloneNotSupportedException {
return super.clone();
}
}

public class CloneAttack {

public static void main(String[] args) throws CloneNotSupportedException {
D d = D.getInstance();
D dClone = (D) d.clone();

System.out.println(d == dClone);

E e = E.getInstance();
E eClone = (E) e.clone();

System.out.println(e == eClone);

// F实例不能访问clone()方法
}
}

结论

  • 枚举类不能覆盖实现clone()方法,因此方案6天然防御该形式攻击
  • 方案1-5为防御克隆攻击,可禁止继承java.lang.Cloneable接口,或者在继承的情况下,在覆盖实现的clone()方法中返回已经生成的实例,而不是重新生成,类似如下代码
1
2
3
4
@Override
public Object clone() throws CloneNotSupportedException {
return instance;
}

3.3、反射

通过反射获取Constructor对象,然后调用newInstance()方法生成实例。

实验代码3:

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
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
import java.lang.reflect.Constructor;

enum L {
SINGLE;

char c;

double d;

L() {
init();
}

public static L getInstance() {
return SINGLE;
}

private void init() {
c = 'a';
d = 20D;
}

}

class J {

private static J instance = new J();

private J() {
}

public static J getInstance() {
return instance;
}
}

class K {

private static K instance;

private static volatile boolean init = false;

private K() {
}

public static synchronized K getInstance() {
if (!init) {
synchronized (E.class) {
if (!init) {
instance = new K();

init = true;
}
}
}

return instance;
}

}

public class ReflectionAttack {
public static void main(String[] args) throws Exception {

Constructor jConstructor = J.class.getDeclaredConstructor();
jConstructor.setAccessible(true);

//返回false
System.out.println(J.getInstance() == jConstructor.newInstance());

// ---分割线---

Constructor kConstructor = K.class.getDeclaredConstructor();
kConstructor.setAccessible(true);

//返回false
System.out.println(K.getInstance() == kConstructor.newInstance());

// ---分割线---

// 枚举类的默认构造方法有“String和int”两个参数
Constructor lConstructor = L.class.getDeclaredConstructor(String.class, int.class);
lConstructor.setAccessible(true);

System.out.println(L.getInstance() == lConstructor.newInstance("AnotherInstance", 1000));
}
}

结论

  • 对于枚举类,在获得其Constructor对象,然后调用newInstance()方法时会直接抛出java.lang.IllegalArgumentException异常,查看方法实现源码可知“枚举类直接被限定不能调用该方法”,因此方案6天然防御该形式攻击
  • 方案1-5为防御反射攻击,对于“饿汉方案”,可在构造方法中增加“重复生成则抛出异常”的判断逻辑,类似如下代码;而对于“懒汉方案”,则无能为力
1
2
3
if (instance != null) {
throw new RuntimeException("the instance exists");
}

3.4、序列化-反序列化

序列化存在多种形式,这里讨论常见的JDK序列化和JSON序列化(以Fastjson序列化为例)。

实验代码4:

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
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
import com.alibaba.fastjson.JSON;

import java.io.*;
import java.lang.reflect.InvocationTargetException;

enum I implements Serializable {
SINGLE;

char c;

double d;

I() {
init();
}

public static I getInstance() {
return SINGLE;
}

private void init() {
c = 'a';
d = 20D;
}

}

class G implements Serializable {

private static G instance = new G();

private G() {
}

public static G getInstance() {
return instance;
}

}

class H implements Serializable {

private static H instance;

private static volatile boolean init = false;

private H() {
}

public static synchronized H getInstance() {
if (!init) {
synchronized (E.class) {
if (!init) {
instance = new H();

init = true;
}
}
}

return instance;
}
}

public class DeserializationAttack {

public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {

jdkDeserializationAttack();

fastjsonDeserializationAttack();

}

private static void jdkDeserializationAttack() {

G g = G.getInstance();

G anotherG = (G) serializeAndDeserializeObjectJDK(g);

//返回false
System.out.println(g == anotherG);

// ---分割线---

H h = H.getInstance();

H anotherH = (H) serializeAndDeserializeObjectJDK(h);

//返回false
System.out.println(h == anotherH);

// ---分割线---
I i = I.getInstance();

I anotherI = (I) serializeAndDeserializeObjectJDK(i);

//返回true
System.out.println(i == anotherI);
}

private static Object serializeAndDeserializeObjectJDK(Serializable serializable) {
try {
ByteArrayOutputStream oo = new ByteArrayOutputStream();

ObjectOutputStream oos = new ObjectOutputStream(oo);

oos.writeObject(serializable);


ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(oo.toByteArray()));

return ois.readObject();
} catch (Exception e) {
e.printStackTrace();

return null;
}
}

private static void fastjsonDeserializationAttack() {
G g = G.getInstance();

G anotherG = (G) serializeAndDeserializeObjectFastjson(g, G.class);

//返回false
System.out.println(g == anotherG);

// ---分割线---

H h = H.getInstance();

H anotherH = (H) serializeAndDeserializeObjectFastjson(h, H.class);

//返回false
System.out.println(h == anotherH);

// ---分割线---
I i = I.getInstance();

I anotherI = (I) serializeAndDeserializeObjectFastjson(i, I.class);

//返回true
System.out.println(i == anotherI);
}

private static Object serializeAndDeserializeObjectFastjson(Object obj, Class clz) {
try {
String jsonStr = JSON.toJSONString(obj);

return JSON.parseObject(jsonStr, clz);
} catch (Exception e) {
e.printStackTrace();

return null;
}
}

}

3.4.1、JDK序列化

类继承java.lang.Serializable接口,通过对一个实例序列化和反序列化,得到一个新的实例。

结论

  • JDK序列化/反序列化对枚举类特殊处理,经过序列化-反序列化过程,不会生成一个新的实例,因此方案6天然防御该形式攻击
  • 方案1-5为防御JDK序列化攻击,可禁止继承java.io.Serializable接口;或者在继承的情况下,覆盖实现readResolve()方法,在该方法中直接返回已经生成的实例。另外需要特别注意的一点是,JDK的反序列化过程无需调用构造方法,因此,不能采用“Fastjson序列化攻击防御”中的“排除定义无参构造方法”方案

3.4.2、Fastjson序列化

通过对一个实例进行Fastjson序列化和反序列化,得到一个新的实例。

结论

  • Fastjson序列化/反序列化对枚举类特殊处理,经过序列化-反序列化过程,不会生成一个新的实例,因此方案6天然防御该形式攻击
  • 方案1-5为防御Fastjson序列化攻击,可采用“排除定义无参构造方法(比如显式定义一个有参构造方法)”方案,本质原因在于Fastjson反序列化时需要调用无参构造方法

3.5、多类加载器加载

多个类加载器加载生成多个不同的Class对象,再生成多个不同的实例对象。

实验代码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
53
54
import java.io.File;
import java.lang.reflect.Method;
import java.net.URL;
import java.net.URLClassLoader;


public class MultiClassLoaderAttack {

public static void main(String[] args) throws Exception {

/**
* 注意先把类路径下的single/Singleton1.class,single/Singleton5.class,single/Singleton6.class文件删除,否则会被优先加载
*/
ClassLoader loader1 = new URLClassLoader(new URL[]{new File("/home/dslztx/Desktop/ca/").toURI().toURL()});
ClassLoader loader2 = new URLClassLoader(new URL[]{new File("/home/dslztx/Desktop/cb/").toURI().toURL()});


Class m01 = loader1.loadClass("single.Singleton1");
Class m02 = loader2.loadClass("single.Singleton1");
comp(m01, m02);

Class n01 = loader1.loadClass("single.Singleton5");
Class n02 = loader2.loadClass("single.Singleton5");
comp(n01, n02);

Class o01 = loader1.loadClass("single.Singleton6");
Class o02 = loader2.loadClass("single.Singleton6");
comp(o01, o02);
}

private static void comp(Class a, Class b) throws Exception {
// 以下结果都为false,因为同一个类被不同类加载器加载得到的Class对象不同

System.out.println(a == b);

Method methodA = a.getMethod("getInstance");
Method methodB = b.getMethod("getInstance");

System.out.println(methodA == methodB);

Object objA = methodA.invoke(null);
Object objB = methodB.invoke(null);

System.out.println(objA == objB);

// 以下结果都为true,因为在一个Class对象范畴中,存在“单例”语义,Class对象也只有一个
System.out.println(methodA.invoke(null) == objA);
System.out.println(methodB.invoke(null) == objB);

System.out.println(objA.getClass() == a);
System.out.println(objB.getClass() == b);
}

}

结论

  • 所有方案都不能避免该形式攻击

四、狭义单例和广义单例

上面所说的单例是“狭义单例”,我们日常所说的单例实际上是“广义单例”,两者的区别在于:

  • 狭义单例,客观确保单例,防止恶意攻击
  • 广义单例,主观确保单例,不考虑恶意攻击,比如“使用Spring框架中的Bean单例模式,实际上Bean对应类的构造方法是public的”

五、“单例”与“只执行一次”

“单例”与“只执行一次”的关系:实现“单例”必然涉及到“只执行一次”,但是“只执行一次”不必然是为了实现“单例”。比如如下代码:

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
public class KafkaProducerFactory {
private static void init() {
if (!init) {
synchronized (KafkaProducerFactory.class) {
if (!init) {
try {

Configuration configuration = ConfigLoadAssist.propConfig(CONFIG_FILE);

// doSomething1()

// doSomething2()

// doSomething3()
} catch (Exception e) {
logger.error("", e);

throw new RuntimeException(e);
} finally {
init = true;
}
}
}
}
}
}

使用哪种“只执行一次”方案来实现“单例”或者用于其他用途,具体情况具体分析。


参考文献

[1]https://cloud.tencent.com/developer/article/1446979
[2]http://wuchong.me/blog/2014/08/28/how-to-correctly-write-singleton-pattern/
[3]https://juejin.im/post/5b50b0dd6fb9a04f932ff53f
[4]http://www.hollischuang.com/archives/2498
[5]http://ifeve.com/from-singleton-happens-before/
[6]http://www.cs.umd.edu/~pugh/java/memoryModel/DoubleCheckedLocking.html
[7]https://www.hollischuang.com/archives/1144
[8]https://www.hollischuang.com/archives/205
[9]https://juejin.im/post/6844903753540173831
[10]https://www.cnblogs.com/giserxiaoliang/p/4922879.html

您的支持将鼓励我继续分享!