volatile的用途
问题: DCL单例需不需要加volatile?
1 | class Instance { |
1.防止指令重排序
- 证明指令重排序存在
1 | private static int x = 0, y = 0; |
- 不存乱序的情况下应该输出的值
1 | x = 1; |
- 存在乱序的情况下出现的值
1 | 第461233次 (0,0) |
- 对象创建的过程
1 | //https://blog.csdn.net/u010737756/article/details/104843152 |
1.使用 idea 的插件jclasslib Bytecode Viewer 即可查看对象实例化过程的字节码
new C();
实列化过程
1 | 0 new #2 <com/yzw/juc/markWord/C> |
new C();
字节码解释
1 | 0 new 创建一个对象,并且其引用进栈 |
- 字节码
0.new ->申请内存。堆里有了一个新的内存。(半初始化。成员变量设置默认值)
graph LR A(c) B[i = 0]
3 dup 因为invokespecial会消耗一份,所以必须先复制一份
4 invokespecial T initlize 初始化,调用他的构造方法
graph LR A(c) B[i = 8]
7 astore_1 把c与i建立关联
graph LR A(c) --> B[i = 8]
8 return 从当前方法返回void
- 结论
由上述步骤可以看出:在多线程的环境下,假设线程1拿到锁Instance.class,线程1执行锁中的代码时有可能会发生 c与i先建立联系 , 然后给i赋值,此时线程2如果出现进入第一个判断ins!= null,此时的线程2就会直接 return ins,所以这里就会出现问题
2.保证线程的可见性
1 | //证明线程的可见性 |
3.cpu为什么会出现乱序执行
CPU缓存的出现主要是为了解决CPU运算速度与内存读写速度不匹配的矛盾,因为CPU运算速度要比内存读写速度快得多,举个例子:
- 一次主内存的访问通常在几十到几百个时钟周期
- 一次L1高速缓存的读写只需要1~2个时钟周期
- 一次L2高速缓存的读写也只需要数十个时钟周期
这种访问速度的显著差异,导致CPU可能会花费很长时间等待数据到来或把数据写入内存。
基于此,现在CPU大多数情况下读写都不会直接访问内存(CPU都没有连接到内存的管脚),取而代之的是CPU缓存,CPU缓存是位于CPU与内存之间的临时存储器,它的容量比内存小得多但是交换速度却比内存快得多。而缓存中的数据是内存中的一小部分数据,但这一小部分是短时间内CPU即将访问的,当CPU调用大量数据时,就可先从缓存中读取,从而加快读取速度。
按照读取顺序与CPU结合的紧密程度,CPU缓存可分为:
- 一级缓存:简称L1 Cache,位于CPU内核的旁边,是与CPU结合最为紧密的CPU缓存
- 二级缓存:简称L2 Cache,分内部和外部两种芯片,内部芯片二级缓存运行速度与主频相同,外部芯片二级缓存运行速度则只有主频的一半
- 三级缓存:简称L3 Cache,部分高端CPU才有
每一级缓存中所存储的数据全部都是下一级缓存中的一部分,这三种缓存的技术难度和制造成本是相对递减的,所以其容量也相对递增。
当CPU要读取一个数据时,首先从一级缓存中查找,如果没有再从二级缓存中查找,如果还是没有再从三级缓存中或内存中查找。一般来说每级缓存的命中率大概都有80%左右,也就是说全部数据量的80%都可以在一级缓存中找到,只剩下20%的总数据量才需要从二级缓存、三级缓存或内存中读取。
缓存一致性协议
为了解决这个问题,在早期的CPU当中,是通过在总线上直接加锁的形式来解决缓存不一致的问题。
但是正如Java中Synchronized一样,直接加锁太粗暴了,由于在锁住总线期间,其他CPU无法访问内存,导致效率低下。很明显这样做是不可取的。
所以就出现了缓存一致性协议。缓存一致性协议有MSI,MESI,MOSI,Synapse,Firefly及DragonProtocol等等。
MESI协议
最出名的就是Intel 的MESI协议,MESI协议保证了每个缓存中使用的共享变量的副本是一致的。
Modify(修改):当缓存行中的数据被修改时,该缓存行置为M状态
Exclusive(独占):当只有一个缓存行使用某个数据时,置为E状态
Shared(共享):当其他CPU中也读取某数据到缓存行时,所有持有该数据的缓存行置为S状态
Invalid(无效):当某个缓存行数据修改时,其他持有该数据的缓存行置为I状态
它核心的思想是:当CPU写数据时,如果发现操作的变量是共享变量,即在其他CPU中也存在该变量的副本,会发出信号通知其他CPU将该变量的缓存行置为无效状态,因此当其他CPU需要读取这个变量时,发现自己缓存中缓存该变量的缓存行是无效的,那么它就会从内存重新读取。
而这其中,监听和通知又基于总线嗅探机制来完成。
4. volatile 是如何保证线程之间的可见性的
总线嗅探机制
嗅探机制其实就是一个监听器,回到我们刚才的流程,如果是加入MESI缓存一致性协议和总线嗅探机制之后:
CPU1读取数据a=1,CPU1的缓存中都有数据a的副本,该缓存行置为(E)状态
CPU2也执行读取操作,同样CPU2也有数据a=1的副本,此时总线嗅探到CPU1也有该数据,则CPU1、CPU2两个缓存行都置为(S)状态
CPU1修改数据a=2,CPU1的缓存以及主内存a=2,同时CPU1的缓存行置为(S)状态,总线发出通知,CPU2的缓存行置为(I)状态
CPU2再次读取a,虽然CPU2在缓存中命中数据a=1,但是发现状态为(I),因此直接丢弃该数据,去主内存获取最新数据
当我们使用volatile关键字修饰某个变量之后,就相当于告诉CPU:我这个变量需要使用MESI和总线嗅探机制处理。从而也就保证了可见性。
5.volatile是如何禁止指令重排序的
字节码层面
ACC_VOLATILEJVM层面
volatile内存区的读写 都加屏障StoreStoreBarrier
volatile 写操作
StoreLoadBarrier
LoadLoadBarrier
volatile 读操作
LoadStoreBarrier
OS和硬件层面
https://blog.csdn.net/qq_26222859/article/details/52235930
hsdis - HotSpot Dis Assembler
windows lock 指令实现 | MESI实现