volatile

# WHY

cpu读取数据过程,从主存到L缓存最后到寄存器(L1和寄存器之间可能存在4字节的Wc写合并缓存)。
主存
主存
L3
L3
L2
L2
L1
L1
0101011010
0101011010
0101011010
0101011010
0101011010
0101011010
0101011010
0101011010
运算单元
运算单元
L2
L2
L1
L1
运算单元
运算单元
0101011010
0101011010
0101011010
0101011010
缓存行一次性读取的字节数
缓存行一次性读取的字节数
Viewer does not support full SVG 1.1

多核或者多cpu分别在线程A和线程B,并且这两个线程同时修改了同一个变量,如果不加限制,最后写会主存的数据就会不可预知。

# 缓存一致性协议

# MESI

MESI仅仅是缓存一致性协议中的一种,MESI规定了缓存行的四种状态。

  1. modified
  2. exclusive
  3. shared
  4. invalid
    当某一线程修改缓存行,此时状态为modified,缓存行lock会通知其他cpu(核心)失效缓存行,转而从主存中重新读取。

# volatile实现

  1. 可见性
  2. 避免指令重排序

# 缓存行验证

cpu每次从内存中取数据,按位取数据影响效率,而且根据局部性原理,使用到旁边数据可能性较大,缓存行过大过小都不合适,一般取64.

img.png

T不继承T0的情况,数组中两个数据是在同一个缓存行,两个线程每次更新两个数据,都会触发缓存行锁或者总线锁,通知L级别缓存失效,耗费大量时间;
T如果集成T0,最后结束时间大大缩减,执行程序可以得到验证。前后对齐方式可以确保volatile修饰的变量不管缓存行起始位置在哪,都能独立存在缓存行中。

# 重排序验证

img.png

按照程序执行顺序,x和y永远不可能同时为0,除非cpu执行上图线程内部的赋值是乱序的。

# 内存屏障

JVM通过指令在volatile修饰变量操作前后增加内存屏障指令(4种),来确保cpu不会发生指令重排。

# 场景

老生常谈的双重检测单例模式; img.png

同步没有加在方法上,是为了锁粒度更小,volatile防止new ConnectionConfig行代码发生指令重排

img.png

通过javap得到字节码,前面数字表示指令偏移量,由于27不依赖21指令完成,有可能先执行27,此时if判断虽然非空,却有可能不是想要获取的对象,volatile关键字 能保证对象的创建过程不会发生指令重排

  • 10:表示加锁
  • 17:分配内存构造对象(半初始化)
  • 21:执行构造方法(此时会初始化程序员赋值的成员变量,之前为该类型的零值)
  • 27:压栈(引用指向分配的内存地址)