一、volatile
首先想到使用volatile变量的场景,就是面试的时候写的单例,先上面试官想要的单例的代码,至于为什么这么写,就不多说了。
|
思考:为什么uniqueInstance变量使用了volatile关键自修饰,如果不用它修饰会导致问题吗?代码验证下
(是不好验证的,要在并发的情况下很多次的实验才可能出现一次)
实际上volatile主要实现了两个功能:
- 保证线程间的可见性
- 禁止指令重排序
1、volatile如何保证线程间的可见性呢?
1.1 什么是可见性:
意思是当一个线程修改一个共享变量时,另外一个线程能立即读到这个修改的值
1.2 可见性的验证:
|
思考:为什么出现这种结果呢?当然是volatile修饰的变量保证了这个变量被修改后,可以被其他的线程及时看到!那么它底层是如何做到的呢?
1.3 可见性的底层实现:
处理器为了提高处理速度,不直接和内存进行通讯,而是先将系统内存的数据读到内部缓存(L1,L2或其他)后再进行操作,但操作完之后不知道何时会写到内存,
如果对声明了Volatile变量进行写操作,JVM就会向处理器发送一条Lock前缀的指令,将这个变量所在缓存行的数据写回到系统内存。但是就算写回到内存,如果其他处理器缓存的值还是旧的,再执行计算操作就会有问题,所以在多处理器下,为了保证各个处理器的缓存是一致的,就会实现缓存一致性协议,每个处理器通过嗅探在总线上传播的数据来检查自己缓存的值是不是过期了,当处理器发现自己缓存行对应的内存地址被修改,就会将当前处理器的缓存行设置成无效状态,当处理器要对这个数据进行修改操作的时候,会强制重新从系统内存里把数据读到处理器缓存里。
存在问题:
如果多个处理器同时修改一个数据,又需要把这个数据写会内存,这种情况怎么办?
.......
2、volatile如何禁止指令重排序?
2.1 指令重排可能带来什么问题?(volatile禁止指令重排的必要性)
首先看一个对象被创建的过程,还是以最开始创建单例的实例来说,上面创建单例实例的关键代码是instance = new Singleton(), 这一行代码可以分解为下面3行伪代码:
memory = allocalte(); // 1.分配对象的内存空间, 这时候对象的值是默认的值 ctorInstance(memory); // 2.初始化对象 instance = memory; // 3. 设置instance指向刚分配的内存地址
上面伪代码中2和3之间可能会被重排序
在单线程的情况下,即使是被重排序,也不会影响正确的结果
但是在多线程下,可能会出现问题,比如重排序后如下:
这样的执行顺序将会使B线程看到一个还未被初始化的对象(或者说是半初始化的状态)。
也就是如果发生重排序,另一个并发执行的线程B将会在(1)处判断instance不为空,接下来B访问instance所引用的对象,但这个对象还未被A线程初始化,B访问到了一个未被初始化的对象!上图就是这个场景。
知晓了问题的根源,两种解决方式:
(1)不允许2、3重排序
(2)允许2、3重排序,但是不允许其他的线程“看到”这个重排序
那么如何禁止指令重排呢,毕竟这些顺序都不是程序员可以控制的,于是volatile又派上了用场
(补充说明:如果单纯的以创建单例的小程序去验证没有volatile因指令重排带来的问题,是不好验证的,要在并发的情况下很多次的实验才可能出现一次)
2.2 volatile禁止指令重排的实现
2.2.1 既然指令重排可能产生问题,为什么底层实现要使用指令重排呢?
这就要重计算机组成原理的底层去理解了,计算机组成的框架图:
cpu和内存的处理速度是相差非常大的,速度比大约1:100。如果顺序执行,其中一个操作内存的指令耗时较长,将会阻塞下面纯cpu计算的指令;如果符合重排的条件,可以将操作内存的指令的执行和cpu计算指令并行节省时间。
先说结论:volatile禁止指令重排是通过加内存屏障实现的。
2.2.2 什么是内存屏障呢?
常说的内存屏障有JVM级别的内存屏障和CPU级别的内存屏障, 这两种是完全不同的概念。
JSR内存屏障(JVM内存屏障):它使CPU或编译器对屏障指令之前和之后发出的内存操作执行一个排序约束。 意味着阻止屏障两侧的指令重排序。
LoadLoad屏障:
对于这样的语句 Load1;LoadLoad; Load2
在Load2及其后续读取操作要读取的数据被访问前,保证Load1要读取的数据被读取完毕
StoreStore屏障:
对于这样的语句 Store1;StoreStore;Store2
在Store2及其后续写操作执行前,保证Store1的写入操作对其他处理器可见
LoadStore屏障:
对于这样的语语句 Load1;LoadStore; Store2
在Store2及其后续写操作被刷出前,保证Load1要读取的数据被读取完毕
StoreLoad屏障:
对于这样的语句 Store1;StoreLoad;Load2
在Load2及其后续读取操作执行前,保证Store1的写入对所有的处理器可见
注意:上面说的4种内存屏障都是CPU的指令
2.2.3 volatile实现细节:
JVM层面:
理解:对volatile变量的写操作或者读操作指令前后加这样的内存屏障
思考:虽然volatile变量在java源码中可能对应一条,但是到汇编指令层级会对应多条指令,那么这个内存屏障是加载哪些指令的前后呢?以创建对象的指令为例:
memory = allocalte(); // 1.分配对象的内存空间, 这时候对象的值是默认的值 ctorInstance(memory); // 2.初始化对象 instance = memory; // 3. 设置instance指向刚分配的内存地址
上面也说了要保证禁止2和3的重排序,我理解是在2和3之间加了个StoreLoad屏障, 1和2之间加了StoreStore屏障
2.2.4 什么是CPU级别的内存屏障
思考上面提到的一个问题,多个处理器同时修改一个数据,又需要把这个数据写会内存,这种情况怎么办?
这个时候就需要用缓冲一致性,不同的CPU支持不同的缓存一致性的实现方案。