14.懒汉单例,双重校验锁再把关

本文详细探讨了如何通过双重校验锁和volatile关键字确保Singleton模式在多线程环境下的线程安全,避免指令重排序带来的问题。通过实例讲解了如何在SingletonThree类中应用这些技术,以提升并发性能并防止异常。
摘要由CSDN通过智能技术生成

单例懒汉实现解决

双重校验锁

理解了线程不安全的原因,以及线程同步的性能消耗原理,因此就需要对其作出改进,在保障线程安全的前提下,又能进一步的减少消耗。
其实不难发现,我们只要保证instance = new SingletonOne(); 是线程互斥访问的就可以保证线程安全了。
那把同步方法加以改造,只用synchronized块包裹这一句。就可以进行:

package com.jachie.singleton;
public class SingletonThree {
	private SingletonThree() {}
	private static SingletonThree instance = null;
	public static SingletonThree getInstance() throws InterruptedException {
		// 第一次判断是否为空
		if(instance == null) {	// 1.
			synchronized (SingletonThree.class){
					instance = new SingletonThree();	// 2.
			}
	}
		return instance;

}
}

还没搞定

可是这样并没有解决问题,A、B线程同时进入代码1.的位置的时候,产生互斥,而此时instance依然是空的,而A和B会分别先后去执行synchronized代码块,创建实例,从而依然会创建两个实例,因此我们需要在代码块里,也就是2.之前再进行一次判断校验,判断instance是否为空,这样第二次执行synchronized代码块的就可以有一个判断不会产生新的实例。

package com.jachie.singleton;
public class SingletonThree {
	private SingletonThree() {}
	private static SingletonThree instance = null;
	public static SingletonThree getInstance() throws InterruptedException {
		// 第一次判断是否为空
		if(instance == null) {
			synchronized (SingletonThree.class){
				// 第二次判断是否为空,防止线程在第一次和第二次之间进行了时间片的转换而导致产生两个实例
				if(instance == null) {
					instance = new SingletonThree();
				}
			}
	}
		return instance;
}
}

如此,当线程A执行结束后,线程B再进入synchronized块后,会先检查一下instance实例是否被创建,这时实例已经被线程A创建过了。所以线程B不会再创建实例,而是直接返回。

JVM重排可能导致异常

原本以为就此结束,可是查阅资料以及访问其他博客发现竟然还有个问题,那就是无序写(out-of-order writes)机制,当前java平台的内存模型,在执行程序时为了提高性能,编译器和处理器常常会对指令做重排序。

而我们创建对象——instance = new SingletonThree();的操作也需要多条指令才可以完成,可以简单划分为:

  1. singleton对象 分配内存空间
  2. 初始化 singleton对象
  3. 将这块内存地址,指向singleton对象

而由于JVM具有指令重排的特性,这只是我们理想创建对象的操作,如果指令进行了重排,执行顺序有可能变成 1-3-2。 指令重排在单线程下不会出现问题,但是在多线程下就会导致返回一个未被初始化的对象:
辟如

线程1执行了1和3两个操作,此时线程2进来进行操作,当它调用getInstance()时的时候发现singleton对象不为空,则返回,而此时的singleton对象并没有初始化,因此容易造成程序的异常。

既然发现了问题,找到了原因,那么就对症下药,因此,根本方法就是组织JVM对指令的重排操作,他该怎么走,就怎么走,按照顺序来搞。

volatile
  • volatile关键字修饰的变量,编译器与运行时都会注意到这个变量是共享的,因此不会将该变量上的操作与其他内存操作一起重排序。
  • volatile变量不会被缓存在寄存器或者对其他处理器不可见的地方,是保证了JVM 每次读变量都从内存中读,跳过 CPU cache 这一步,因此在读取volatile类型的变量时总会返回最新写入的值。
  • volatile在Java并发编程中常用于保持内存可见性防止指令重排序

那么,这样我们就比较好理解,需要要加入Volatile 变量了。由于Volatile禁止JVM对指令进行重排序。所以创建对象的过程仍然会按照指令1-2-3的有序执行。

最终优化

不多说,干他

package com.jachie.singleton;

public class SingletonThree {
	private SingletonThree() {}
	// 使用 volatile 修饰保证不会被重排序
	private volatile static SingletonThree instance = null;
	public static SingletonThree getInstance() throws InterruptedException {
		// 第一次判断是否为空
		if(instance == null) {
			synchronized (SingletonThree.class){
				// 第二次判断是否为空,防止线程在第一次和第二次之间进行了时间片的转换
				if(instance == null) {
					instance = new SingletonThree();
				}
			}
	}
		return instance;
}
}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

空 白

停止的只会是自己,你得追逐世界

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值