高并发学习之06关键字volatile

	本文参考《Java并发编程的艺术》

1. volatile 简介

在上一篇文章中我们了解了synchronized关键字,并了解并发编程JMM中synchronized是怎么保证原子性、可见性、有序性。在这里在啰嗦下,在 Java 并发编程中,要想使并发程序能够正确地执行,必须要保证三条原则,即:原子性、可见性和有序性。只要有一条原则没有被保证,就有可能会导致程序运行不正确。
上篇中我们了解了synchronized原理,知道synchronized有锁升级的机制,但是不管怎么synchronized都是java中重量级的数据同步机制。这一篇我们就详细了解下关键字voldatile.
volatile 是java提供的一种最轻量级的同步机制。volatile关键字被用来保证可见性,可见性的意思是当一个线程修改一个共享变量时,另外一个线程能读到这个修改的值。

2. volatile原理

volatile变量修饰的共享变量,在进行写操作的时候会多出一个lock前缀的汇编指令,这个指令在前面我们讲解CPU高速缓存的时候提到过,会触发总线锁或者缓存锁,通过缓存一致性协议来解决可见性问题。
对于声明了volatile的变量进行写操作,JVM就会向处理器发送一条Lock前缀的指令,把这个变量所在的缓存行的数据写回到系统内存,再根据我们前面提到过的MESI的缓存一致性协议,来保证多CPU下的各个高速缓存中的数据的一致性。
简单的说volatile实现了三点:

  • Lock前缀的指令会引起处理器缓存写回内存
  • 一个处理器的缓存回写到内存会导致其他处理器的缓存失效
  • 处理器发现本地缓存失效后,就会从内存中重读该变量数据,即可以获取当前最新值。

那么在JMM中的理解其实就是:关键字volatile可以用来修饰字段(成员变量),就是告知程序任何对该变量的访问均需要从共享内存中获取,而对它的改变必须同步刷新回共享内存,它能保证所有线程对变量访问的可见性。

3. 简单了解下JMM中定义的happens-before规则

JSR-133(JDK 5开始)使用happens-before的概念来指定两个操作之间的执行顺序。由于这两个操作可以在一个线程之内,也可以是在不同线程之间。因此,JMM可以通过happens-before关系向程序员提供跨线程的内存可见性保证(如果A线程的写操作a与B线程的读操作b之间存在happens-before关系,尽管a操作和b操作在不同的线程中执行,但JMM向程序员保证a操作将对b操作可见)。具体的定义为:

  • 如果一个操作happens-before另一个操作,那么第一个操作的执行结果将对第二个操作可见,而且第一个操作的执行顺序排在第二个操作之前。

  • 两个操作之间存在happens-before关系,并不意味着Java平台的具体实现必须要按照happens-before关系指定的顺序来执行。如果重排序之后的执行结果,与按happens-before关系来执行的结果一致,那么这种重排序并不非法(也就是说,JMM允许这种重排序)。

上面的第一点是JMM对程序员的承诺。

从程序员的角度来说,可以这样理解happens-before关系:如果A happens-before B,那么Java内存模型将向程序员保证——A操作的结果将对B可见,且A的执行顺序排在B之前。注意,这只是JMM向程序员做出的保证!

上面的第二点是JMM对编译器和处理器重排序的约束原则。这里我们就不深入了。

4. volatile写-读建立的happens-before关系

从内存语义的角度来说,volatile的写-读与锁的释放-获取有相同的内存效果:volatile写和锁的释放有相同的内存语义;volatile读与锁的获取有相同的内存语义。

class VolatileExample {
	int a = 0;
	volatile boolean flag = false;
	public void writer() {
		a = 1;     // 1
		flag = true;   // 2
	}
	public void reader() {
		if (flag) {    // 3
		int i = a;  // 4
		……
		}
   }
}

假设线程A执行writer()方法之后,线程B执行reader()方法。根据happens-before规则,这个过程建立的happens-before关系可以分为3类:
1)根据程序次序规则,1 happens-before 2; 3 happens-before 4。
2)根据volatile规则,2 happens-before 3。
3)根据happens-before的传递性规则,1 happens-before 4。
上述happens-before关系的图形化表现形式如下:
volatile线程间规则
图中每一个箭头两个节点就代码一个happens-before关系,黑色的代表根据程序顺序规则推导出来,红色的是根据volatile变量的写happens-before 于任意后续对volatile变量的读,而蓝色的就是根据传递性规则推导出来的。这里的2 happen-before 3,同样根据happens-before规则定义:如果A happens-before B,则A的执行结果对B可见,并且A的执行顺序先于B的执行顺序,我们可以知道操作2执行结果对操作3来说是可见的,也就是说当线程A将volatile变量 flag更改为true后线程B就能够迅速感知。

5.volatile写-读的内存语义

volatile写的内存语义

  • 当写一个volatile变量时,JMM会把该线程对应的本地内存中的共享变量值刷新到主内存。

volatile读的内存语义

  • 当读一个volatile变量时,JMM会把该线程对应的本地内存置为无效。线程接下来将从主内存中读取共享变量。

对于写的理解,我们已上面示例程序VolatileExample为例,假设线程A首先执行writer()方法,随后线程B执行reader()方法,初始时两个线程的本地内存中的flag和a都是初始状态。下图是线程A执行volatile写后,共享变量的状态示意图。
共享变量的状态
线程A在写flag变量后,本地内存A中被线程A更新过的两个共享变量的值
被刷新到主内存中。此时,本地内存A和主内存中的共享变量的值是一致的。

对于读的内存语义理解为:
读的内存语义
如图所示,在读flag变量后,本地内存B包含的值已经被置为无效。此时,线程B必须从主内存中读取共享变量。线程B的读取操作将导致本地内存B与主内存中的共享变量的值变成一致。
如果我们把volatile写和volatile读两个步骤综合起来看的话,在读线程B读一个volatile变量后,写线程A在写这个volatile变量之前所有可见的共享变量的值都将立即变得对读线程B可见。

6.volatile内存语义的实现

6.1 指令重排序

JMM内存模型提到过重排序分为编译器重排序和处理器重排序。为了实现volatile内存语义,JMM会分别限制这两种类型的重排序类型。下表是JMM针对编译器制定的volatile重排序规则表。
volatile重排序规则表
通过上表可以看出:

  • 当第二个操作是volatile写时,不管第一个操作是什么,都不能重排序。这个规则确保volatile写之前的操作不会被编译器重排序到volatile写之后。
  • 当第一个操作是volatile读时,不管第二个操作是什么,都不能重排序。这个规则确保volatile读之后的操作不会被编译器重排序到volatile读之前。
  • 当第一个操作是volatile写,第二个操作是volatile读时,不能重排序。
6.2 内存屏障

JMM内存模型中我们简单的了解了下内存屏障。那么通过前面的知识点,是不是可以猜猜volatile是不是就是通过内存屏障的方式来保证其语义的实现。
在JMM中把内存屏障指令分为4类,通过在不同的语义下使用不同的内存屏障来进制特定类型的处理器重排序,从而来保证内存的可见性
JMM内存屏障指令

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章