第6章 单例模式与多线程
1. 饿汉模式和懒汉模式
所谓饿汉模式,就是形容“着急”,“急迫”的意思,即在类对象实例化之前,单例对象已经创建完毕,也称为“立即加载”模式。1
2
3
4
5
6
7
8
9
10
11
12class MyObj {
public static MyObj singleton =new MyObj();
MyObj() {
}
public static MyObj getInstance() {
return singleton;
}
}
而所谓懒汉模式,是形容“不着急”、“缓慢”的意思,即在想要获取到单例对象时,如果不存在,就创建一个,也称为“延迟加载”模式。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15class MyObj {
public static MyObj singleton = null;
MyObj() {
}
public static MyObj getInstance() {
if (singleton == null) {
singleton = new MyObj();
}
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
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43package ch06;
/**
* @ClassName HungryTest
* @Deacription // TODO
* @Author LiuZhian
* @Date 2019-11-23 18:20
* @Version 1.0
**/
public class SingletonTest {
public static void main(String[] args) {
Thread ths[] = new MyThread[3];
for (int i = 0; i < 3; i++) {
ths[i] = new MyThread();
ths[i].start();
}
}
}
class MyObj {
public static MyObj singleton = null;
MyObj() {
}
public static MyObj getInstance() {
if (singleton == null) {
singleton = new MyObj();
}
return singleton;
}
}
class MyThread extends Thread {
public void run() {
System.out.println(MyObj.getInstance().hashCode());
}
}
运行上述后,输出的hashCode相同,即三个线程拿到的都是同一个对象,这时没出现问题,因为每个线程执行的时间很快,小于被分配到的时间片,不至于被CPU中断。
此后,我们对代码稍加修改,在新建单例时,模拟创建前的准备工作(这很有可能是很耗时的操作),如下:1
2
3
4
5
6
7
8
9
10
11
12public static MyObj getInstance() {
if (singleton == null) {
try {
// 模拟创建前的准备工作
Thread.sleep(3000);
singleton = new MyObj();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
return singleton;
}
这样一来,三个线程输出的HashCode就不一样了,因为在线程A发现单例对象为空时,会尝试去创建一个单例,而在准备工作时又被中断,单例实际上还没创建出来。线程B和线程C也一样,都会去尝试创建,于是最终创建出了3个“单例”。
2. 多线程下懒汉模式的解决方案
想解决多线程下懒汉模式的问题,很简单的办法就是直接对方法或者整个方法块上锁,但是这样效率比较低。
我不得不再次吐槽这本书,书上说什么,啊,只对重要的方法块上锁,还什么效率会大幅度提升,balabala的,像如下这样:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15public static MyObj getInstance() {
if (singleton == null) {
try {
// 模拟创建前的准备工作
Thread.sleep(3000);
// 只上锁部分代码块
synchronized (MyObj.class) {
singleton = new MyObj();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
return singleton;
}
这简直就是放屁好么?你在锁个什么东西,这样不一样还是会出现多个线程都各自新建单例对象么?我不知道为什么这会被当做是一种方法,你讨论一种错误的方法到底是什么意思?完全错误好么!!我真的想不通为什么要把这个列出来。
2.1 双检查锁
当然,好在还算良心,告诉了有“双检查锁”的解决方案,这个的确没问题。回想一下,为什么会出现多个线程各自创建单例对象?是因为它们在被分到CPU时间片的时候都发现obj==null
,然后在准备创建单例对象之前又被中断。所以,我们必须让这个单例对象的引用让各个线程实时可见,即加上volatile
关键字。然后在准备工作创建完毕后,真正创建单例对象之前,也做一次判断。即所谓的“双检查锁”。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16public static MyObj getInstance() {
if (singleton == null) {
try {
// 模拟创建前的准备工作
Thread.sleep(3000);
synchronized (MyObj.class) {
if (singleton == null) {
singleton = new MyObj();
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
return singleton;
}
2.2 静态内置类
1 | class MyObj { |
外部类加载时并不需要立即加载内部类,内部类不被加载则不去初始化singleton,故而不占内存。即当MyObj类第一次被加载时,并不需要去加载InnerSingletonBuilder类,只有当getInstance()方法第一次被调用时,才会导致虚拟机加载InnerSingletonBuilder类,从而才会初始化singleton,这种方法不仅能确保线程安全,也能保证单例的唯一性,同时也延迟了单例的实例化。
2.3 使用枚举类
1 |
|
枚举在java中与普通类一样,都能拥有字段与方法,而且枚举实例创建是线程安全的,在任何情况下,它都是一个单例。