java单例模式-指令重排陷阱

本文探讨了懒汉式单例模式的线程不安全性,重点介绍了双重检查锁(DCL)的概念及其潜在问题。通过分析内存分配顺序,揭示了并发环境下未初始化对象的风险。最后,讲解了如何使用volatile关键字确保可见性和避免并发问题,提升单例模式的正确性和性能。
摘要由CSDN通过智能技术生成

一、案例

public class SingletonTest {
	 private static SingletonTest SingletonTest;
	 private SingletonTest() {
	 }
	 public static SingletonTest getInstance() {
	    if (SingletonTest == null) {
	        SingletonTest = new SingletonTest();
	    }
	    return SingletonTest;
	 }
}

这是一个懒汉式的单例实现,众所周知,因为没有相应的锁机制,这个程序是线程不安全的,实现安全的最快捷的方式是添加 synchronized

public class SingletonTest {
	 private static SingletonTest SingletonTest;
	 private SingletonTest() {
	 }
	 public static synchronized  SingletonTest getInstance() {
	    if (SingletonTest == null) {
	        SingletonTest = new SingletonTest();
	    }
	    return SingletonTest;
	 }
}

使用synchronized之后,可以保证线程安全,但是synchronized将全部代码块锁住,这样会导致较大的性能开销,因此,人们想出了一个“聪明”的技巧:双重检查锁DCL(double checked locking)的机制实现单例。

public class SingletonTest {
	 private static SingletonTest SingletonTest;
	 private SingletonTest() {
	 }
	 public static synchronized  SingletonTest getInstance() {
		 if (SingletonTest == null) {
	            synchronized (SingletonTest.class) {
	                if (SingletonTest == null) {
	                    SingletonTest = new SingletonTest();
	                }
	            }
	        }
	        return SingletonTest;
	 }
}

如上面代码所示,如果第一次检查instance不为null,那么就不需要执行下面的加锁和初始化操作。因此可以大幅降低synchronized带来的性能开销。上面代码表面上看起来,似乎两全其美:

  1. 在多个线程试图在同一时间创建对象时,会通过加锁来保证只有一个线程能创建对象。

  2. 在对象创建好之后,执行getInstance()将不需要获取锁,直接返回已创建好的对象。

程序看起来很完美,但是这是一个不完备的优化,在线程执行到第9行代码读取到instance不为null时(第一个if),instance引用的对象有可能还没有完成初始化。

问题的根源

问题出现在创建对象的语句singleton3 = new Singleton3(); 上,在java中创建一个对象并非是一个原子操作,可以被分解成三行伪代码:

//1:分配对象的内存空间
memory = allocate();
//2:初始化对象
ctorInstance(memory);  
//3:设置instance指向刚分配的内存地址
instance = memory;   

上面三行伪代码中的2和3之间,可能会被重排序(在一些JIT编译器中),即编译器或处理器为提高性能改变代码执行顺序,这一部分的内容稍后会详细解释,重排序之后的伪代码是这样的:

//1:分配对象的内存空间
memory = allocate(); 
//3:设置instance指向刚分配的内存地址
instance = memory;
//2:初始化对象
ctorInstance(memory);

在单线程程序下,重排序不会对最终结果产生影响,但是并发的情况下,可能会导致某些线程访问到未初始化的变量。

模拟一个2个线程创建单例的场景,如下表:

时间线程A线程B
t1A1:分配对象内存空间
t2A3:设置instance指向内存空间
t3B1:判断instance是否为空
t4B2:由于instance不为null,线程B将访问instance引用的对象
t5A2:初始化对象
t6A4:访问instance引用的对象

按照这样的顺序执行,线程B将会获得一个未初始化的对象,并且自始至终,线程B无需获取锁!

双重检查锁问题解决方案

解决方案就是大名鼎鼎的volatile关键字,对于volatile我们最深的印象是它保证了”可见性“,它的”可见性“是通过它的内存语义实现的:

  • 写volatile修饰的变量时,JMM会把本地内存中值刷新到主内存

  • 读volatile修饰的变量时,JMM会设置本地内存无效

public class SingletonTest {
	 private static volatile  SingletonTest SingletonTest;
	 private SingletonTest() {
	 }
	 public static synchronized  SingletonTest getInstance() {
		 if (SingletonTest == null) {
	            synchronized (SingletonTest.class) {
	                if (SingletonTest == null) {
	                    SingletonTest = new SingletonTest();
	                }
	            }
	        }
	        return SingletonTest;
	 }
}

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

wwwzhouzy

你的鼓励将是我创作的最大动力

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

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

打赏作者

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

抵扣说明:

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

余额充值