目录(DCL必须要加volatile)
- 什么是单例设计模式
- 两种单例模式比较
- 实例(Runtime)
- DCL的引入
- DCL一定要加volatile
<一> 什么是单例设计模式
确保类在内存中只有一个对象,并且该对象由类自动创建并且对外提供;
<二> 两种单例模式
▶ 饿汉式
- 私有构造函数
- 创建私有静态类变量并new赋值
- 创建公有静态方法 getXxx,内部返回私有静态类变量
package kyleeo.util_01;
/*
* 单例设计模式一
*/
public class Student {
private Student() {}
private static Student s = new Student();
public static Student getStudent() {
return s;
}
}
▶ 懒汉式
- 私有构造函数
- 创建私有静态类变量=null
- 创建公有静态方法 getXxx,需要使用synchronized约束方法
- 判断私有静态类变量是否为null,为null则表示第一次创建,new
- 判断私有静态类变量是否为null,不为null则直接返回私有静态类变量
package kyleeo.util_01;
/*
* 单例延迟加载模式
*/
public class Teacher {
private Teacher() {}
private static Teacher t = null;
public static synchronized Teacher getTeacher() {
if(t == null) {
t = new Teacher();
}
return t;
}
}
<三> Runtime
1> System.exit(0) 底层调用的是Runtime中的exit()方法
2> Runtime中exit()调用的是Shutdown中的exit()方法
3> Shutdown中的exit()方法又调用了halt()方法
这里的halt()方法将会调用halt0本地方法
4> 这里我们要说道Runtime类,该类是单例模式的典型代表
5> 那么Runtime这个类究竟是干什么的呢?
Runtime类是JVM运行时的代码表示:
2020-10-13补充 ↓
四:DCL的引入
1> 什么是DCL?
Double Check Lock
2> 在上述“懒汉式”单例模式中,直接使用synchronized修饰整个方法(锁的粒度很大,部分业务逻辑也被锁住是不合适的)
public class Teacher {
private Teacher() {}
private static Teacher t = null;
public static synchronized Teacher getTeacher() {
// 业务逻辑
// 业务逻辑
// 业务逻辑
// 业务逻辑
if(t == null) {
t = new Teacher();
}
return t;
}
}
3> 此时我们使用synchronized锁住代码块,而不是锁住整个方法
public class Teacher {
private Teacher() {}
private static Teacher t = null;
public static Teacher getTeacher() {
// 业务逻辑
// 业务逻辑
// 业务逻辑
// 业务逻辑
if(t == null) {
synchronized (Teacher.class) {
t = new Teacher();
}
}
return t;
}
/*
2040686731
1052444169
1052444169
1052444169
1052444169
1052444169
1052444169
1052444169
1052444169
1052444169
*/
public static void main(String[] args) {
for(int i=0;i<10;i++){
new Thread(()->{
System.out.println(Teacher.getTeacher().hashCode());
}).start();
}
}
}
4> 上述的代码在多线程环境下仍然是不安全的:
假设线程A执行到getTeacher方法的if(t == null)被线程B抢占,线程B优先级非常高直接将函数执行完(锁也已经释放掉了),此时线程A抢到了CPU开始执行后方的代码,此时已经创建了两个对象,出现了背离单例模式的情况,如何避免呢?于是出现了DCL即双重检测
5> DCL
/*
* 单例模式DCL
*/
public class Teacher {
private Teacher() {}
private static Teacher t = null;
public static Teacher getTeacher() {
// 业务逻辑
// 业务逻辑
if(t == null) {
synchronized (Teacher.class) {
if(t==null) {
t = new Teacher();
}
}
}
return t;
}
}
五:DCL一定要加volatile(阻止指令重排序)
上方的代码,如果Teacher不加volatile,那么假设当线程A执行到 t = new Teacher(); 语句指令发生了重排序,在astore执行后,B线程发现t对象已经不是null了(半初始化状态),于是B线程又创建了一个Teacher对象,原线程随后也创建了自己的Teacher对象,此时背离了单例模式的情况,如何解决呢?)
→ 使用volatile修饰t对象,volatile有两个功能:
1. 确保线程的可见性(一个线程修改了从主内存中copy的变量会强制刷新到主内存,同时在每次操作volatile变量时会重主内存中重新拷贝一份到当前线程中)
2. 禁止指令重排序
与volatile类比,synchronized可以保持代码的原子性,因为保证了原子性,所以也从另一个角度确保了线程的可见性;
/*
* 单例模式DCL - 使用volatile禁止指令重排序
*/
public class Teacher {
private Teacher() {}
private static volatile Teacher t = null;
public static Teacher getTeacher() {
// 业务逻辑
// 业务逻辑
if(t == null) {
synchronized (Teacher.class) {
if(t==null) {
t = new Teacher();
}
}
}
return t;
}
}
六:volatile如何禁止指令重排序
禁止指令重排序主要是为了防止多线程读写数据出现异常,所以出现了四种内存屏障:
1. readread
read1 readread read2
2. readwrite
read1 readwrite write1
3. writeread
write1 writeread read1
4. writewrite
write1 writewrite write2
在内存屏障前后的语句不能跨越内存屏障执行,前面指令执行完毕后后方指令才允许被执行;
但是还有一种更加好理解的方式:直接在汇编层面使用lock指令修饰读写指令,通过控制MC内存控制器产生信号以锁住数据总线,确保当前只有自己线程在操作(JVM中似乎是这种实现)。