volatile关键字作用与内存可见性、指令重排序概述[JAVA]

版权声明:本文为博主原创文章,转载请注明出处 https://blog.csdn.net/t894690230/article/details/50588129

在理解volotile关键字的作用之前,先粗略解释下内存可见性与指令重排序。

####1. 内存可见性####

Java内存模型规定,对于多个线程共享的变量,存储在主内存当中,每个线程都有自己独立的工作内存,并且线程只能访问自己的工作内存,不可以访问其它线程的工作内存。工作内存中保存了主内存中共享变量的副本,线程要操作这些共享变量,只能通过操作工作内存中的副本来实现,操作完毕之后再同步回到主内存当中,其JVM内存模型大致如下图。

这里写图片描述

而JAVA内存模型规定工作内存与主内存之间的交互协议,其中包括8种原子操作:

  1. lock:将主内存中的变量锁定,为一个线程所独占
  2. unclock:将lock加的锁定解除,此时其它的线程可以有机会访问此变量
  3. read:将主内存中的变量值读到工作内存当中
  4. load:将read读取的值保存到工作内存中的变量副本中。
  5. use:将值传递给线程的代码执行引擎
  6. assign:将执行引擎处理返回的值重新赋值给变量副本
  7. store:将变量副本的值存储到主内存中。
  8. write:将store存储的值写入到主内存的共享变量当中。

其中lock和unlock定义了一个线程访问一次共享内存的界限,而其它操作下线程的工作内存与主内存的交互大致如下图所示。

这里写图片描述

从上图可以看出,read and load 主要是将主内存中数据复制到工作内存中,use and assign则主要是使用数据,并将改变后的值写入到工作内存,store and write则是用工作内存数据刷新主存相关内容。

但是以上的一系列操作并不是原子的,也既是说在read and load之后,主内存中变量的值发生了改变,这时再use and assign则并不是取的最新的值,而我认为的内存可见性可粗略描述为,如果数据A在一个线程中的改变能够立即被其他线程可见,那么则说数据A具有内存可见性 ,也既是说如果数据A具有内存可见性,那么即使一个线程在read and load之后,数据A的值被改变了,在use and assign时也能获取到数据A最新的值并使用,那么该如何保证线程在每次use and assign时都是获取的数据A的最新的值呢?

其实只要线程在每次use and assign时都是直接从主内存中获取数据A的值,就能够保证每次use and assign都是获取的数据A的最新的值,也既是能保证数据A的内存可见性,而volatile关键字的作用之一便是系统每次用到被它修饰过的变量时都是直接从主内存当中提取,而不是从Cache中提取,同时对于该变量的更改会马上刷新回主存,以使得各个线程取出的值相同,这里的Cache可以理解为线程的工作内存。当然了volatile关键字还有另外一个非常重要的作用,即局部阻止指令重排序。

(注:synchronized或其它加锁,也能保证内存可见性,但实现方式略有不同,也不在本文的讨论范围内)

####2. 指令重排序####

首先看下以下线程A和线程B的部分代码:

线程A:
content = initContent();	//(1)
isInit = true;				//(2)
线程B
while (isInit) {			//(3)
    content.operation();    //(4)
}

从常规的理解来看,上面的代码是不会出问题的,但是JVM可以对它们在不改变数据依赖关系的情况下进行任意排序以提高程序性能(遵循as-if-serial语义,即不管怎么重排序,单线程程序的执行结果不能被改变),而这里所说的数据依赖性仅针对单个处理器中执行的指令序列和单个线程中执行的操作,不同处理器之间和不同线程之间的数据依赖性不会被编译器和处理器考虑,也即是说对于线程A,代码(1)和代码(2)是不存在数据依赖性的,尽管代码(3)依赖于代码(2)的结果,但是由于代码(2)和代码(3)处于不同的线程之间,所以JVM可以不考虑线程B而对线程A中的代码(1)和代码(2)进行重排序,那么假设线程A中被重排序为如下顺序:

线程A:
isInit = true;				//(2)
content = initContent();	//(1)

对于线程B,则可能在执行代码(4)时,content并没有被初始化,而造成程序错误。那么应该如何保证绝对的代码(2) happens-before 代码(3)呢?没错,仍然可以使用volatile关键字。

volatile关键字除了之前提到的保证变量的内存可见性之外,另外一个重要的作用便是局部阻止重排序的发生,即保证被volatile关键字修饰的变量编译后的顺序与 也即是说如果对isInit使用了volatile关键字修饰,那么在线程A中,就能保证绝对的代码(1) happens-before 代码(2),也便不会出现因为重排序而可能造成的异常。

####3. 总结####
综上所诉,volatile关键字最主要的作用是:

  1. 保证变量的内存可见性
  2. 局部阻止重排序的发生

####4. 附录 - happens-before原则####

英文原文:

  • Each action in a thread happens before everyaction in that thread that comes later in the program’s order.
  • An unlock on a monitor happens before everysubsequent lock on that same monitor.
  • A write to a volatile field happens before everysubsequent read of that same volatile.
  • A call to start() on a thread happens before anyactions in the started thread.
  • All actions in a thread happen before any otherthread successfully returns from a join() on that thread.

中文描述:

  • 程序顺序规则:一个线程中的每个操作,happens-before 于该线程中的任意后续操作。 (并没有)
  • 监视器锁规则:对一个监视器锁的解锁,happens-before 于随后对这个监视器锁的加锁。
  • volatile变量规则:对一个volatile域的写,happens-before 于任意后续对这个volatile域的读。
  • 传递性:如果A happens-before B,且B happens-before C,那么A happens-before C。
  • Thread.start()的调用会happens-before于启动线程里面的动作。
  • Thread中的所有动作都happens-before于其他线程从Thread.join中成功返回。

其中第一条程序顺序规则并不合理,因为在线程中只有存在数据依赖性才不会被重排序,而没有任何数据依赖性的操作,依然可能被编译器重排序。

####5. 参考文献####

[1] Brian Goetz.Java并发编程实战.机械工业出版社.2012
[2] http://ifeve.com/easy-happens-before/
[3] http://www.infoq.com/cn/articles/java-memory-model-2/
[4] http://www.cnblogs.com/mengyan/archive/2012/08/22/2651575.html
[5] http://my.oschina.net/chihz/blog/58035
[6] http://www.cnblogs.com/aigongsi/archive/2012/04/01/2429166.html
[7] http://ifeve.com/jvm-reordering/
[8] …

以上仅为个人学习所记笔记,如有错误,欢迎指正

展开阅读全文

没有更多推荐了,返回首页