延迟加载(懒汉模式)

什么是延迟加载?延迟加载就是字啊调用get()方法时实例才被创建,常见的实现方法就是在get()方法中进行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
public class MyObject {
private static MyObject myObject;
private MyObject() {
}
public static MyObject getInstance() {
// 延迟加载
if (myObject != null) {
} else {
myObject = new MyObject();
}
return myObject;
}
}
public class MyThread extends Thread {
@Override
public void run() {
System.out.println(MyObject.getInstance().hashCode());
}
}
public class Run {
public static void main(String[] args) {
MyThread t1 = new MyThread();
t1.start();
}
}

运行结果如图1所示,虽然取得一个对象的实例,但如果是在多线程的环境中,就会出现取出多个实例的情况,与单例模式的初衷是相背离的。



图1 “饿汉模式”成功取出一个实例

延迟加载/”懒汉模式“的缺点

前面的两个测试虽然使用了“立即加载”和“延迟加载”实现了单例设计模式,但在多线程的环境中,前面“延迟加载”示例中的代码完全就是错误的,根本不能实现保存单例的状态。来看一下如何在多线程环境中结合“错误的单例模式”创建出“多例”。
代码清单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
public class MyObject {
private static MyObject myObject;
private MyObject() {
}
public static MyObject getInstance() {
try {
if (myObject != null) {
} else {
// 模拟在创建对象之前做一些准备性的工作
Thread.sleep(3000);
myObject = new MyObject();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
return myObject;
}
}
public class MyThread extends Thread {
@Override
public void run() {
System.out.println(MyObject.getInstance().hashCode());
}
}
public class Run {
public static void main(String[] args) {
MyThread t1 = new MyThread();
MyThread t2 = new MyThread();
MyThread t3= new MyThread();
t1.start();
t2.start();
t3.start();
}
}

运行结果如图2所示,打印出来了3种hasCode,说明创建出了3个对象,并不是单例的,这就是“错误的单例模式”。



图2 非单例设计模式

延迟加载/“懒汉模式”的解决方案

声明synchronized关键字

在代码清单2中public static MyObject getInstance()前加上synchronized。运行结果如图3所示。



图3 运行结果

此方法加入同步synchronized关键字得到相同实例的对象,但此方法的运行效率非常低下,是同步运行的,下一个线程想要取得对象,则必须等上一个线程释放锁之后,才可以继续运行。

尝试同步方法块

同步方法是对方法的整体进行持锁,这对运行效率来讲是不利的。改成同步代码块能解决吗?
代码清单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
public class MyObject {
private static MyObject myObject;
private MyObject() {
}
public static MyObject getInstance() {
try {
synchronized (MyObject.class) {
if (myObject != null) {
} else {
// 模拟在创建对象之前做一些准备性的工作
Thread.sleep(3000);
myObject = new MyObject();
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
return myObject;
}
}
public class MyThread extends Thread {
@Override
public void run() {
System.out.println(MyObject.getInstance().hashCode());
}
}
public class Run {
public static void main(String[] args) {
MyThread t1 = new MyThread();
MyThread t2 = new MyThread();
MyThread t3= new MyThread();
t1.start();
t2.start();
t3.start();
}
}

运行结果如图4所示,此方法加入同步synchronized语句块得带相同实例的对象,但此方法的运行效率也是非常低的,和synchronized同步方法一样是同步运行的。继续更改代码尝试解决这个缺点。



图4 运行结果

针对某些重要的代码进行单独的同步

同步代码块可以针对某些重要的代码进行单独的同步,而其他的代码则不需要同步。这样在运行时,效率完全可以得到大幅提升,对代码清单4中的代码进行修改。
代码清单5 修改MyObject类

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 MyObject {
private static MyObject myObject;
private MyObject() {
}
public static MyObject getInstance() {
try {
if (myObject != null) {
} else {
// 模拟在创建对象之前做一些准备性的工作
Thread.sleep(3000);
//使用synchronized (MyObject.class)
//虽然部分代码被上锁
//但还是有非线性安全问题
synchronized (MyObject.class) {
myObject = new MyObject();
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
return myObject;
}
}

运行结果如图5所示,此方法使用同步synchronized语句块,只对实例化对象的关键代码进行同步,从语句的结构来讲,运行的效率的确得到了提升。但如果是遇到多线程的情况下还是无法解决得到同一个实例对象的结果。到底如何解决“懒汉模式”遇到多线程的情况呢?



图5 运行结果

使用DCL双检查锁机制

在最后的步骤中,使用的是DCL双检查锁机制来实现多线程环境中的延迟加载单例设计模式。对代码清单4中的代码进行修改。
代码清单6 修改MyObject类

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 MyObject {
private static MyObject myObject;
private MyObject() {
}
public static MyObject getInstance() {
try {
if (myObject != null) {
} else {
// 模拟在创建对象之前做一些准备性的工作
Thread.sleep(3000);
synchronized (MyObject.class) {
if (myObject == null) {
myObject = new MyObject();
}
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
return myObject;
}
}

运行结果如图6所示,使用双重检查锁功能,成功地解决了“懒汉模式”遇到多线程的问题。DCL也是大多数线程结合单例模式使用的解决方案



图6 运行结果

Java多线程编程核心技术源代码

Adhere to the original technology to share, your support will encourage me to continue to create!