摘要:但是有引入了新的问题线程不安全,返回的对象可能还没有初始化。如果只有一个线程调用是没有问题的因为不管步骤如何调换,保证返回的对象是已经构造好了。这种特殊情况称之为指令重排序采用了允许将多条指令不按程序规定的顺序分开发送给各相应电路单元处理。
双重检测锁的演变过程 synchronized修饰方法的单例模式目录
双重检测锁的演变过程
利用HappensBefore分析并发问题
无volatile的双重检测锁
双重检测锁的最初形态是通过在方法声明的部分加上synchronized进行同步,保证同一时间调用方法的线程只有一个,从而保证new Singlton()的线程安全:
public class Singleton { private static Singleton instance; private Singleton() { } public static synchronized Singleton getInstance() { if (instance == null) { instance = new Singleton(); } return instance; } }
这样做的好处是代码简单、并且JVM保证new Singlton()这行代码线程安全。但是付出的代价有点高昂:
所有的线程的每一次调用都是同步调用,性能开销很大,而且new Singlton()只会执行一次,不需要每一次都进行同步。
既然只需要在new Singlton()时进行同步,那么把synchronized的同步范围缩小呢?
线程不安全的双重检测锁public class Singleton { private static Singleton instance; private Singleton() { } public static Singleton getInstance() { if (instance == null) { synchronized (Singleton.class) { if (instance == null) { instance = new Singleton(); } } } return instance; } }
把synchronized同步的范围缩小以后,貌似是解决了每次调用都需要进行同步而导致的性能开销的问题。但是有引入了新的问题:线程不安全,返回的对象可能还没有初始化。
深入到字节码的层面来看看下面这段代码:
instance = new Singleton() returen instance;
正常情况下JVM编译成成字节码,它是这样的:
step.1 new:开辟一块内存空间 step.2 invokespecial:执行初始化方法,对内存进行初始化 step.3 putstatic:将该内存空间的引用赋值给instance step.4 areturn:方法执行结束,返回instance
当然这里限定在正常情况下,在特殊情况下也可以编译成这样:
step.1 new:开辟一块内存空间 step.3 putstatic:将该内存空间的引用赋值给instance step.2 invokespecial:执行初始化方法,对内存进行初始化 step.4 areturn:方法执行结束,返回instance
步骤2和步骤3进行了调换:先执行步骤3再执行步骤2。
如果只有一个线程调用是没有问题的:因为不管步骤如何调换,JVM保证返回的对象是已经构造好了。
如果同时有多个线程调用,那么部分调用线程返回的对象有可能是没有构造好的对象。
这种特殊情况称之为:指令重排序:CPU采用了允许将多条指令不按程序规定的顺序分开发送给各相应电路单元处理。当然不是乱排序,重排序保证CPU能够正确处理指令依赖情况以保障程序能够得出正确的执行结果。
利用HappensBefore分析并发问题 什么是HappensBeforeHappensBefore:先行发生,是
判断数据是否存在竞争、线程是否安全的重要依据
A happens-beforeB,那么A对B可见(A做的操作对B可见)
是一种偏序关系。hb(a,b),hb(b,c) => hb(a,c)
换句话说,可以通过HappensBefore推断代码在多线程下是否线程安全
举一个《深入理解Java虚拟机》上的例子:
//以下操作在线程A中执行 int i = 1; //以下操作在线程B中执行 j = i; //以下操作在线程C中执行 i = 2;
如果hb(i=1,j=i),那么可以确定变量j的值一定等于1。得出这个结论的依据有两个:
根据HappensBefore的规则,i=1的结果可以被j=i观察到
线程C还没有登场
如果线程C的执行时间在线程A和线程B之间,那么j的值是多少呢?答案是不确定!因为线程C和线程B之间没有HappensBefore的关系:线程C对变量的i的更改可能被线程B观察到也可能不会!
HappensBefore关系这些是“天然的”、JVM保证的HappensBefore关系:
程序次序规则
管程锁定规则
volatile变量规则
线程启动规则
线程终止规则
线程中断规则
对象终结规则
传递性
重点介绍程序次序规则,管程锁定规则,volatile变量规则,传递性,后面分析需要用到这四个性质:
程序次序规则:在一个线程内,按照程序控制流顺序,书写在前面的操作HappensBefore书写在后面的操作
管程锁定规则:对于同一个锁来说,在时间顺序上,上一个unlock操作HappensBefore下一个lock操作
volatile变量规则:对于一个volatile修饰的变量,在时间顺序上,写操作HappensBefore读操作
传递性:hb(a,b),hb(b,c) => hb(a,c)
分析之前线程不安全的双重检测锁public class Singleton { private static Singleton instance; private Singleton() { } public static Singleton getInstance() { if (instance == null) { //1 synchronized (Singleton.class) { //2 if (instance == null) { //3 instance = new Singleton(); //4 new //4.1 invokespecial //4.2 pustatic //4.3 } } } return instance; //5 } }
经过上面的讨论,已经知道因为JVM重排序导致代码4.2提前执行了,导致后面一个线程执行代码1返回的值为false,进而直接返回了还没有构造好的instance对象:
线程1 | 线程2 |
---|---|
1 | |
2 | |
3 | |
4.1 | |
4.3 | |
1 | |
5 | |
4.2 | |
5 |
通过表格,可能清晰看到问题所在:线程1代码4.3 执行后,线程2执行代码1读到了脏数据。要想不读到脏数据,只要证明存在hb(T1-4.3,T2-1)(T1-4表示线程1代码4,T2-1表示线程2代码1,下同),那么是否存在呢?很遗憾,不存在:
程序次序规则:不在同一个线程
管程锁定规则:线程2没有尝试lock
volatile变量规则:instance对象没有通过volatile关键字修饰
传递性:不存在
用HappensBefore分析,可以很清晰、明确看到没有volatile修饰的双重检测锁是线程不安全的。但,真的是这样的吗?
无volatile的双重检测锁在第二部分,通过HappensBefore分析没有volatile修饰的双重检测锁是线程不安全,那只有用volatile修饰的双重检测锁才是线程安全的吗?答案是否定的。
用volatile关键字修饰的本质是想利用volatile变量规则,使得写操作(T1-4)HappensBefore读操作(T2-1),那只要另找一条HappensBefore规则保证即可。答案是程序次序规则和管程锁定规则
先看代码:
public class Singleton { private static Singleton instance; private Singleton() { } public static Singleton getInstance() { if (instance == null) { //1 synchronized (Singleton.class) { //2 if (instance == null) { //3 Singleton temp = new Singleton(); //4 temp.toString(); //5 instance = temp; //6 } } } return instance; //7 } }
在原有的基础上加了两行代码:
instance = new Singleton(); //4 Singleton temp = new Singleton(); //4 temp.toString(); //5 instance = temp; //6
为什么要这么做?
通过管程锁定规则保证执行到代码6时,temp对象已经构造好了。想一想,为什么?
其他线程执行代码1时,如果能够观察到T1-6的写操作,那么直接返回instance对象
如果没有观察到T1-6的写操作,那么尝试获取锁,此时管程锁定规则开始生效:保证当前线程一定能够观察到T1-6操作
执行流程可能是这样的:
线程1 | 线程2 | 线程3 |
---|---|---|
1 | ||
1 | ||
2 | ||
3 | ||
4 | ||
5 | ||
6 | ||
2 | ||
3 | ||
1 | 7 | |
7 | ||
7 |
无论怎样执行,其他线程都能够观察到T1-6的写操作
其他 volatile、synchronized为什么可以禁止JVM重排序内存屏障。
JVM在凡是有volatile、synchronized出现的地方都加了一道内存屏障:重排序时,不可以把内存屏障后面的指令重排序到内存屏障前面执行,并且会及时的将线程工作内存中的数据及时更新到主内存中,进而使得其他的线程能够观察到最新的数据
参考资料
《深入理解Java虚拟机》
文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。
转载请注明本文地址:https://www.ucloud.cn/yun/74288.html
摘要:代码实现单例模式静态变量保存全局实例私有构造函数,防止外界实例化对象私有克隆函数,防止外界克隆对象静态方法,单例统一访问路口单例模式的优缺点优点改进系统的设计是对全局变量的一种改进缺点难于调试隐藏的依赖关系无法用错误类型的数据覆写一个单例 单例模式(Singleton Pattern 单件模式或单元素模式)单例模式有以下3个特点:1、一个类只能有一个类对象(只能实例化一个对象)2、它必...
摘要:代码实现单例模式静态变量保存全局实例私有构造函数,防止外界实例化对象私有克隆函数,防止外界克隆对象静态方法,单例统一访问路口单例模式的优缺点优点改进系统的设计是对全局变量的一种改进缺点难于调试隐藏的依赖关系无法用错误类型的数据覆写一个单例 单例模式(Singleton Pattern 单件模式或单元素模式)单例模式有以下3个特点:1、一个类只能有一个类对象(只能实例化一个对象)2、它必...
摘要:代码实现单例模式静态变量保存全局实例私有构造函数,防止外界实例化对象私有克隆函数,防止外界克隆对象静态方法,单例统一访问路口单例模式的优缺点优点改进系统的设计是对全局变量的一种改进缺点难于调试隐藏的依赖关系无法用错误类型的数据覆写一个单例 单例模式(Singleton Pattern 单件模式或单元素模式)单例模式有以下3个特点:1、一个类只能有一个类对象(只能实例化一个对象)2、它必...
摘要:的构造函数实际上负责了两件事情。有一个缺点,假如我们某天需要利用这个类,在页面中创建千千万万个,即要这个类从单例类变成一个普通的可产生多个实例的类,那我们就要改写构造函数,把控制创建唯一对象的那一段去掉,这样会给我们带来不必要的麻烦。 定义:单例模式保证一个类仅有一个实例,并提供一个访问它的全局访问点。 单例模式是一种常用的模式,有一些对象我们往往只需要一个,比如线程池、全局缓存、浏览...
阅读 1026·2021-11-22 15:33
阅读 3310·2021-11-08 13:20
阅读 1287·2021-09-22 10:55
阅读 2013·2019-08-29 11:08
阅读 729·2019-08-26 12:24
阅读 3019·2019-08-23 17:15
阅读 2156·2019-08-23 16:12
阅读 1886·2019-08-23 16:09