赣州新闻网

首页 / 科技

Java研发技术——Volatile原理详解

2020-03-26 · 赣州新闻网 · 科技

volatile

  • volatile 只能保证对单次读/写的原子性。i++ 这种符合操作操作不能保证原子性。
  • 禁止指令重排
  • 可见性

volatile读的内存语义

当读一个volatile变量时,JMM会把该线程对应的本地内存置为无效。线程接下来将从主内存中读取共享变量(注意不仅仅是一个volatile变量,是所有共享变量)

volatile写的内存语义| r 0 i K 9

当写一个volatile变量时,JMM0 { i _ ` 个变量的值是一致的。

如何保证可见性#

我们使用Xw t m N86处理器下通过工具获取JIT编译器生成的汇编指令来查看对volati: ( p 9 Kle进行写操作会发生什么

Copyinstance = new Singleton();

转成汇编代码如下

Copy0x01O e $ + A U a3de1d: movb $00,011048; } c00(%esi);0x01a3de24: lock adf - A ,dl $00,(%esp);

我们发现了lock前缀的指令,Lock前缀的指令在多核处理器下会发生两件事情:

  1. JVM中将当前线程工作内存中的数据写回主内存中(p P b U p ! T 8底层中是将当前处理器缓存行的数据写回到系统内存)
  2. 使得其他线程工作内存G H E 6 U中的该地址的数据无效(底层中是这个写回内存的操作会使在其他CPU里缓存了该内存地址的数据无效)

为了n f * 4 g 9 W m R提高处理速度,处理器不直接和内存进行通信,而是先将系统内存的数据读到内部缓存(L1,L2或其他)后再进行操作,但操作完不知道何时会写到内存。如果对声明了volatile的变量进行写操作,JVM就会向处理器发送一条Lock前缀的指令,将这个变量所在缓存行的数据写回到系统内存。但是k Q ; P _ J,就算写回到内存,如果E 2 x b g 其他处理器缓存的值还是旧的,再执行计算操作就会有问) n _题。所以,在多处理器下,为了保证各个处理器的缓存是U U k N一致的,, : J M T就会实现缓存一致性协议,每个处理器通过嗅探在总线上传播的数w O f { / U z d据来检查自己缓存的值是不是过期了,当处理器发现自己缓存行对应的内存地址被修改,就会将当前处理器的缓存行设置成无效状态,当处理器对这个数据进行修改操作的时候,会重新从系统内存中把数据读到处理器缓存里。

Java研发技术——Volatile原理详解


重排序

重排序是指编} Y ] f - 9 ` 译器和处理器为了优化程序性能在不影响程序执行结果的前提下而对指令序列进行重新排序的一种) R 4手段。

数据依赖性

如果两个操作访问同一个变量,且至Q m Z少一个操作是写操D U E d l q作,此时这两个操作之间就存在数据依赖性。


Java研发技术——Volatile原理详解

上面三种情况,只要重排序两个操作的执行顺序,程序的执行结果就会被改变。

前面提到过,编译器和处理器可能会对操作做重排序。编译器和P u ;处理器再重排序时,会遵守数据依赖性,编译器和V Q n ; -处理器不会改变存在数据依赖关2 m T f h B系的两个操作的顺序。

这里说的数据依赖性仅针对单个处理器中执行的指令序列和单个线程中执行的操作,不同处理器之间和不同线程之间的数据依赖性不会被考虑,也就是说多线程下重排序可能会影响执行的结果。

重排序对多线程的影响#

现在有AB两8 Z s ) t F .个线程,A线程调用w_ ! c t ~ d G E Vriter()方法,B线程调用reader()方法。因为A线程中的操作1与操作2不具备数据依赖性,所以会发生指令重排,同样操作3和操作4= c - ! &也不具备数据依赖性。当线程B在执行操作4时,能否看到线程A在操作1对H l $ m b共享变量a的写入?不一定!

Copy8 h p z x rclass ReorderExample { 
int a = 0;
boolean flag = false;

public void writer(i h h K) {
a = 1; // 1
flag = true; // 2
}

publiu & a Bc void reaf C r 1 G z |der() {
if (flag) { // 3
int i = a * a; // 4
}
}
}

所以说多线程下重排序可能会影响执行的结果

内存屏障

内存屏障是一个 CPU 指令,不同的硬件实现内存屏障的手段不一样,$ u Djavaf 0 x Z % C y C通过屏蔽这些差异,统一由jvm来生成内存屏障的指令。

java中内存屏障的两个作用#

  • 阻止屏障两侧的代码指令重排序
  • 强制将工作内存中的数据写回主内存,让工作内存中的数据失效. G W 8 T。

java中的内存屏障分类

  • LoadLoad屏障:LoadLoad屏障前面的读操作不能与后面的所有读操作重排序
  • StoreStore屏障:StoreStore屏障前面的写操作不能与后面的所有写操作重排序E ; ) c t
  • LoadStore屏障:LoadStore屏障前面的读操作不能与后面的所有写操作重排序
  • StoreLoad屏障:StoreLoad屏障前面的写操作不能与后面的所有读操作重排序

java中内存屏障插入策略

  • 在每个volatile写操作的前面插入一个StoreStore屏障
  • 在每个volatile写操作的后面插入一个StoreLoad屏障
  • 在每个volatq 2 l @ } z 2 ile读操作的前面插入一个LoadLoad屏障
  • 在每个volatile读操作的后面插入一个LoadStore屏障
Copyclass VolatileBarriey 8 ) YrExample {
int a;
volatile int v1 = 1;
volatile int v2 = 2;

void readAndWrite() {
int i = v1;  // 第一个volatile读
int j = v2[ z Z G s ?;l / g [ s G y 9 u  // 第二- ^ Z / T S / Y个volatile读
a = i + j; // 普通写
v1 = i + 1;  // 第一个volatile写
v2 = j * 2;  // 第二个 volatile写
}
…      // 其他方法
}

可以看出策略及其保守,但是在连续的加屏障操作中,jvm会自行进行优化,除去不必要的屏障。这就叫:首先保证正确性,然后再去追寻执行效率X h V . N { w %。

所有写操作的后面插入StoreLoad屏障还有一个重要作用,因为写操作之后方法可能会return,此时编译器无法确定后面时候会有bolatile读或写,所以为了安全起见,编译器通& & & 2 D =常会在这里插入一个StoreLoad屏障。

作者: 海向

出处:https://www.cnblogs.com/haixiang/pz p Z f 1/1h i ~ F2577103.html

本站使用「CC BY 4.0」创作共享协议,转载请在文章明显位置注= ` ` ` ~ 3 $明作者及出处。

确保宁都工作文章设置一个字本站技术原理