为什么要是有DCL单例模式,相信大家很多写的单例模式都是未加锁的常规懒汉模式,其实这种写法在单线程没什么影响,一旦使用在多线程中就会出现创建多个对象,这样就不是单例模式了,为了能够在多线程中也是单例模式我们引入了DCL单例模式。
介绍DCL单例模式之前,我们先了解一下volatile关键字:
Volatile修饰符修饰的变量在编译器编译时确保本条指令不会因编译器的优化而省略。
主要特征:
- 保证了不同线程同时对变量修改时实时可见性,即一个线程修改了该变量,其他线程实时可见;
- 禁止指令重排序(尤为重要);
DCL单例指令流程:
- 为对象分配内存;
- 初始化实例对象;
- 为对象的引用分配内存;
由于JVM为了优化指令,提高程序运行效率,允许指令重排序,如果JVM优化指令为1、3、2顺序执行,那么多线程同时执行任务时,如果指令3刚好执行完指令2未来及执行,其他线程调getInstance()执行任务时就会抛出对象未被初始化异常;所以为了解决这个问题我们就引用了Volatile关键字来修饰变量。
public class SingleDemo {
private static volatile SingleDemo instance;
private SingleDemo() {
}
public static SingleDemo getInstance() {
if (instance == null) {//不用volatile修饰的话,线程二提前访问指令顺序被打乱,认为不为空跳过判断
synchronized (SingleDemo.class) {
if (instance == null) {
instance = new SingleDemo();//不用volatile修饰的话,线程一指令顺序被打乱未来及创建对象
}
}
}
return instance;//不用volatile修饰的话,线程二直接拿到instance
}
}
看了上面代码估计会有同学问为什么要把同步锁这样写在方法内部而不是直接写在外部方法名上,像这样:
public static synchronized SingleDemo getInstance() {
if (instance == null) {
instance = new SingleDemo();
}
return instance;
}
这样虽然保证了多线程安全,但是会导致很大的性能开销,加锁只需要在第一次初始化的时候用到就行,之后的调用都没必要再进行加锁,所以为了解决这个问题DCL(双重锁定检查)单例模式就诞生了。当然同步锁也可以防止反射破坏实例化单例对象;
可能还有同学会问为同步锁啥不用synchronized (this),像这样:
public static SingleDemo getInstance() {
if (instance == null) {//不用volatile修饰的话,线程二提前访问指令顺序被打乱,认为不为空跳过判断
synchronized (this) {
if (instance == null) {
instance = new SingleDemo();//不用volatile修饰的话,线程一指令顺序被打乱未来及创建对象
}
}
}
return instance;//不用volatile修饰的话,线程二直接拿到instance
}
这就牵扯了锁类型:方法锁、对象锁、类锁:
- 方法锁 就是例如synchronized 修饰方法的public static synchronized SingleDemo getInstance(),作用是多线程同时调用该方法时只能通过,作用域在所有对象调用该方法时;
- 对象锁 就是例如synchronized (this),作用是多线程同时调用该对象锁时只能通过一个,如果是多个对象责失效,作用域值在本对象内;
- 类锁 就是例如synchronized (SingleDemo.class),作用是多线程同时创建对象时只能通过一个,作用域是类仅存在创建对象时,对象调用时失效;
所以上面使用对象锁写法是不正确的,这里是实例化创建对象,应该使用类锁。
欢迎关注微信公众号!你的每个赞和在看,都是对我的支持!👍