出处:  Java Volatile关键字

  Java的volatile关键字用于标记一个变量“应当存储在主存”。更确切地说,每次读取volatile变量,都应该从主存读取,而不是从CPU缓存读取。每次写入一个volatile变量,应该写到主存中,而不是仅仅写到CPU缓存。

  实际上,从Java 5开始,volatile关键字除了保证volatile变量从主存读写外,还提供了更多的保障。我将在后面的章节中进行说明。

变量可见性问题

  Java的volatile关键字能保证变量修改后,对各个线程是可见的。这个听起来有些抽象,下面就详细说明。

  在一个多线程的应用中,线程在操作非volatile变量时,出于性能考虑,每个线程可能会将变量从主存拷贝到CPU缓存中。如果你的计算机有多个CPU,每个线程可能会在不同的CPU中运行。这意味着,每个线程都有可能会把变量拷贝到各自CPU的缓存中,如下图所示:

  对于非volatile变量,JVM并不保证会从主存中读取数据到CPU缓存,或者将CPU缓存中的数据写到主存中。这会引起一些问题,在后面的章节中,我会来解释这些问题。

  试想一下,如果有两个以上的线程访问一个共享对象,这个共享对象包含一个counter变量,下面是代码示例:

  1. public class SharedObject {
  2. public int counter = 0;
  3. }

  如果只有线程1修改了(自增)counter变量,而线程1和线程2两个线程都会在某些时刻读取counter变量。

  如果counter变量没有声明成volatile,则counter的值不保证会从CPU缓存写回到主存中。也就是说,CPU缓存和主存中的counter变量值并不一致,如下图所示:

  这就是“可见性”问题,线程看不到变量最新的值,因为其他线程还没有将变量值从CPU缓存写回到主存。一个线程中的修改对另外的线程是不可见的。

volatile可见性保证

  Java的volatile关键字就是设计用来解决变量可见性问题。将counter变量声明为volatile,则在写入counter变量时,也会同时将变量值写入到主存中。同样的,在读取counter变量值时,也会直接从主存中读取。

下面的代码演示了如果将counter声明为volatile:

  1. public class SharedObject {
  2. public volatile int counter = 0;
  3. }

  将一个变量声明为volatile,可以保证变量写入时对其他线程的可见。

  在上面的场景中,一个线程(T1)修改了counter,另一个线程(T2)读取了counter(但没有修改它),将counter变量声明为volatile,就能保证写入counter变量后,对T2是可见的。

  然而,如果T1和T2都修改了counter的值,只是将counter声明为volatile还远远不够,后面会有更多的说明。

完整的volatile可见性保证

  实际上,volatile的可见性保证并不是只对于volatile变量本身那么简单。可见性保证遵循以下规则:

  如果线程A写入一个volatile变量,线程B随后读取了同样的volatile变量,则线程A在写入volatile变量之前的所有可见的变量值,在线程B读取volatile变量后也同样是可见的。

  如果线程A读取一个volatile变量,那么线程A中所有可见的变量也会同样从主存重新读取。

下面用一段代码来示例说明:

  1. public class MyClass {
  2. private int years;
  3. private int months
  4. private volatile int days;
  5.  
  6. public void update(int years, int months, int days){
  7. this.years = years;
  8. this.months = months;
  9. this.days = days;
  10. }
  11. }

  update()方法写入3个变量,其中只有days变量是volatile。

  完整的volatile可见性保证意味着,在写入days变量时,线程中所有可见变量也会写入到主存。也就是说,写入days变量时,years和months也会同时被写入到主存。

下面的代码读取了years、months、days变量:

  1. public class MyClass {
  2. private int years;
  3. private int months
  4. private volatile int days;
  5.  
  6. public int totalDays() {
  7. int total = this.days;
  8. total += months * 30;
  9. total += years * 365;
  10. return total;
  11. }
  12.  
  13. public void update(int years, int months, int days){
  14. this.years = years;
  15. this.months = months;
  16. this.days = days;
  17. }
  18. }

  请注意totalDays()方法开始读取days变量值到total变量。在读取days变量值时,months和years的值也会同时从主存读取。因此,按上面所示的顺序读取时,可以保证读取到days、months、years变量的最新值。

''译者注:可以将对volatile变量的读写理解为一个触发刷新的操作,写入volatile变量时,线程中的所有变量也都会触发写入主存。而读取volatile变量时,也同样会触发线程中所有变量从主存中重新读取。因此,应当尽量将volatile的写入操作放在最后,而将volatile的读取放在最前,这样就能连带将其他变量也进行刷新。上面的例子中,update()方法对days的赋值就是放在years、months之后,就是保证years、months也能将最新的值写入到主存,如果是放在两个变量之前,则days会写入主存,而years、months则不会。反过来,totalDays()方法则将days的读取放在最前面,就是为了能同时触发刷新years、months变量值,如果是放后面,则years、months就可能还是从CPU缓存中读取值,而不是从主存中获取最新值。''

指令重排问题

出于性能考虑,JVM和CPU是允许对程序中的指令进行重排的,只要保证(重排后的)指令语义一致即可。如下代码为例:

  1. int a = 1;
  2. int b = 2;
  3.  
  4. a++;
  5. b++;

这些指令可以按以下顺序重排,而不改变程序的语义:

  1. int a = 1;
  2. a++;
  3.  
  4. int b = 2;
  5. b++;

然而,指令重排面临的一个问题就是对volatile变量的处理。还是以前面提到的MyClass类来说明:

  1. public class MyClass {
  2. private int years;
  3. private int months
  4. private volatile int days;
  5.  
  6. public void update(int years, int months, int days){
  7. this.years = years;
  8. this.months = months;
  9. this.days = days;
  10. }
  11. }

一旦update()变量写了days值,则years、months的最新值也会写入到主存。但是,如果JVM重排了指令,比如按以下方式重排:

  1. public void update(int years, int months, int days){
  2. this.days = days;
  3. this.months = months;
  4. this.years = years;
  5. }

  在days被修改时,months、years的值也会写入到主存,但这时进行写入,months、years并不是新的值(译者注:即在months、years被赋新值之前,就触发了这两个变量值写入主存的操作,自然这两个变量在主存中的值就不是新值)。新的值自然对其他线程是不可见的。指令重排导致了程序语义的改变。

Java对此有一个解决方法,我们会在下一章节中说明。

Java volatile Happens-Before保证

  为了解决指令重排的问题,Java的volatile关键字在可见性之外,又提供了happends-before保证。happens-before原则如下:

  如果有读写操作发生在写入volatile变量之前,读写其他变量的指令不能重排到写入volatile变量之后。写入一个volatile变量之前的读写操作,对volatile变量是有happens-before保证的。注意,如果是写入volatile之后,有读写其他变量的操作,那么这些操作指令是有可能被重排到写入volatile操作指令之前的。但反之则不成立。即可以把位于写入volatile操作指令之后的其他指令移到写入volatile操作指令之前,而不能把位于写入volatile操作指令之前的其他指令移到写入volatile操作指令之后。

  如果有读写操作发生在读取volatile变量之后,读写其他变量的指令不能重排到读取volatile变量之前。注意,如果是读取volatile之前,有读取其他变量的操作,那么这些操作指令是有可能被重排到读取volatile操作指令之后的。但反之则不成立。即可以把位于读取volatile操作指令之前的指令移到读取volatile操作指令之后,而不能把位于读取volatile操作指令之后的指令移到读取volatile操作指令之前。

以上的happens-before原则为volatile关键字的可见性提供了强制保证。

''译者注:这两个原则读起来有些拗口(当然翻译也不足够好),其实就是不管JVM怎么去禁止/允许某些情况下的指令重排,最终就是保证“完整的volatile可见性保证”的那种效果,所以,只要理解了“完整的volatile可见性保证”的效果就足够了 ''

volatile并不总是可行的 

  虽然volatile关键字能保证volatile变量的所有读取都是直接从主存读取,所有写入都是直接写入到主存中,但在一些情形下,仅仅是将变量声明为volatile还是远远不够的。

  就像前面示例所说的,线程1写入共享变量counter的值,将counter声明为volatile已经足够保证线程2总是能获取到最新的值。

  事实上,多个线程都能写入共享的volatile变量,主存中也能存储正确的变量值,然而这有一个前提,变量新值的写入不能依赖于变量的旧值。换句话说,就是一个线程写入一个共享volatile变量值时,不需要先读取变量值,然后以此来计算出新的值。

  如果线程需要先读取一个volatile变量的值,以此来计算出一个新的值,那么volatile变量就不足够保证正确的可见性。(线程间)读写volatile变量的时间间隔很短,这将导致一个竞态条件,多个线程同时读取了volatile变量相同的值,然后以此计算出了新的值,这时各个线程往主存中写回值,则会互相覆盖。

  多个线程对counter变量进行自增操作就是这样的情形,下面的章节会详细说明这种情形。

  设想一下,如果线程1将共享变量counter的值0读取到它的CPU缓存,然后自增为1,而还没有将新值写回到主存。线程2这时从主存中读取的counter值依然是0,依然放到它自身的CPU缓存中,然后同样将counter值自增为1,同样也还没有将新值写回到主存。如下图所示:

  从实际的情况来看,线程1和线程2现在就是不同步的。共享变量counter正确的值应该是2,但各个线程中CPU缓存的值都是1,而主存中的值依然是0。这是很混乱的。即使线程最终将共享变量counter的值写回到主存,那值也明显是错的。

总结:

volatile关键字可以达到的效果:

当一个变量被定义为volatile之后,它对所有的线程就具有了可见性,也就是说当一个线程修改了该变量的值,所有的其它线程都可以立即知道,可以从两个方面来理解这句话:

  1.线程对变量进行修改之后,要立刻回写到主内存。

  2.线程对变量读取的时候,要从主内存中读,而不是工作内存。

但是这并不意味着使用了volatile关键字的变量具有了线程安全性,举个栗子:

  1. public class AddThread implements Runnable {
  2. private volatile int num=0;
  3. @Override
  4. public void run() {
  5. for (int i=1;i<=10000;i++){
  6. num=num+1;
  7. System.out.println(num);
  8. }
  9. }
  10. }
  11.  
  12. public class VolatileThread {
  13. public static void main(String[] args) {
  14. Thread[] th = new Thread[20];
  15. AddThread addTh = new AddThread();
  16. for(int i=1;i<=20;i++){
  17. th[i] = new Thread(addTh);
  18. th[i].start();
  19. }
  20. }
  21. }

这里我们创建了20个线程,每个线程对num进行10000次累加。

按理结果应该是打印1,2,3.。。。。。200000 。

但是结果却是1,2,3…..x ,x小于200000.

为什么会是这样的结果?

我们仔细分析一下这行代码:num=num+1;

虽然只有一行代码,但是被编译为字节码以后会对应四条指令:

&amp;amp;lt;img src="https://pic2.zhimg.com/50/v2-f8284d04bd0f4baad7506b81d9c66599_hd.jpg" data-caption="" data-size="normal" data-rawwidth="181" data-rawheight="104" class="content_image" width="181"/&amp;amp;gt;

1.Getstatic将num的值从主内存取出到线程的工作内存

2.Iconst_1 和 iadd 将num的值加一

3.Putstatic将结果同步回主内存

在第一步Getstatic将num的值从主内存取出到线程的工作内存因为num加了Volatile关键字,可以保证它的值是正确的,但是在执行第二步的时候其它的线程有可能已经将num的值加大了。在第三步就会将较小的值同步到内存,于是造成了我们看到的结果。

既然如此,Volatile在什么场合下可以用到呢?

一个变量,如果有多个线程只有一个线程会去修改这个变量,其它线程都只是读取该变量的值就可以使用Volatile关键字,为什么呢?一个线程修改了该变量,其它线程会立刻获取到修改后的值。

因为Volatile的特性可以保证这些线程获取到的都是正确的值,而他们又不会去修改这个变量,不会造成该变量在各个线程中不一致的情况。当然这种场合也可以用synchronized关键字

当运算结果并不依赖变量的当前值的时候该变量也可以使用Volatile关键字,上栗子:

  1. public class shutDownThread implements Runnable {
  2. volatile boolean shutDownRequested;
  3. public void shutDown(){
  4. shutDownRequested = true;
  5. }
  6. @Override
  7. public void run() {
  8. while (!shutDownRequested) {
  9. System.out.println("work!");
  10. }
  11. }
  12. }
  13.  
  14. public class Demo01 {
  15. public static void main(String[] args) throws InterruptedException {
  16. Thread[] th = new Thread[10];
  17. shutDownThread t = new shutDownThread();
  18. for(int i=0;i<=9;i++){
  19. th[i] = new Thread(t);
  20. th[i].start();
  21. }
  22. Thread.sleep(2000);
  23. t.shutDown();
  24. }
  25. }
  当调用t.shutDown()方法将shutDownRequested的值设置为true以后,因为shutDownRequested 使用了volatile ,所有线程都获取了它的最新值true,while循环的条件“!shutDownRequested”不再成立,“ System.out.println("work!");”打印work的代码也就停止了执行。

何时使用volatile

  正如我前面所说,如果两个线程同时读写一个共享变量,仅仅使用volatile关键字是不够的。你应该使用 synchronized 来保证读写变量是原子的。(一个线程)读写volatile变量时,不会阻塞(其他)线程进行读写。你必须在关键的地方使用synchronized关键字来解决这个问题。

  除了synchronized方法,你还可以使用java.util.concurrent包提供的许多原子数据类型来解决这个问题。比如,AtomicLong或AtomicReference,或是其他的类。

  如果只有一个线程对volatile进行读写,而其他线程只是读取变量,这时,对于只是读取变量的线程来说,volatile就已经可以保证读取到的是变量的最新值。如果没有把变量声明为volatile,这就无法保证。

  volatile关键字对32位和64位的变量都有效。

volatile的性能考量

  读写volatile变量会导致变量从主存读写。从主存读写比从CPU缓存读写更加“昂贵”。访问一个volatile变量同样会禁止指令重排,而指令重排是一种提升性能的技术。因此,你应当只在需要保证变量可见性的情况下,才使用volatile变量。

Java Volatile关键字(转)的更多相关文章

  1. [Java并发编程(三)] Java volatile 关键字介绍

    [Java并发编程(三)] Java volatile 关键字介绍 摘要 Java volatile 关键字是用来标记 Java 变量,并表示变量 "存储于主内存中" .更准确的说 ...

  2. 13、Java并发性和多线程-Java Volatile关键字

    以下内容转自http://tutorials.jenkov.com/java-concurrency/volatile.html(使用谷歌翻译): Java volatile关键字用于将Java变量标 ...

  3. Java volatile 关键字底层实现原理解析

    本文转载自Java volatile 关键字底层实现原理解析 导语 在Java多线程并发编程中,volatile关键词扮演着重要角色,它是轻量级的synchronized,在多处理器开发中保证了共享变 ...

  4. Java volatile关键字详解

    Java volatile关键字详解 volatile是java中的一个关键字,用于修饰变量.被此关键修饰的变量可以禁止对此变量操作的指令进行重排,还有保持内存的可见性. 简言之它的作用就是: 禁止指 ...

  5. Java Volatile关键字

    在当前的Java内存模型下,线程可以把变量保存在本地内存(比如机器的寄存器)中,而不是直接在主存中进行读写. 这就可能造成一个线程在主存中修改了一个变量的值,而另外一个线程还继续使用它在寄存器中的变量 ...

  6. 从根源上解析 Java volatile 关键字的实现

    1.解析概览 内存模型的相关概念 并发编程中的三个概念 Java内存模型 深入剖析Volatile关键字 使用volatile关键字的场景 2.内存模型的相关概念 缓存一致性问题.通常称这种被多个线程 ...

  7. java volatile关键字解析

    volatile是什么 volatile在java语言中是一个关键字,用于修饰变量.被volatile修饰的变量后,表示这个变量在不同线程中是共享,编译器与运行时都会注意到这个变量是共享的,因此不会对 ...

  8. Java Volatile关键字 以及long,double在多线程中的应用

    概念: volatile关键字,官方解释:volatile可以保证可见性.顺序性.一致性. 可见性:volatile修饰的对象在加载时会告知JVM,对象在CPU的缓存上对多个线程是同时可见的. 顺序性 ...

  9. Java volatile关键字的用法

    volatile不能解决同步问题 如果想要理解volatile关键字的作用不得不先了解Java内存模型 摘抄一下来自百度百科的话 在本次线程内,当读取一个变量时,为提高存取速度,编译器优化时有时会先把 ...

随机推荐

  1. AcWing:176. 装满的油箱(bfs + dijiskla思想)

    有N个城市(编号0.1…N-1)和M条道路,构成一张无向图. 在每个城市里边都有一个加油站,不同的加油站的单位油价不一样. 现在你需要回答不超过100个问题,在每个问题中,请计算出一架油箱容量为C的车 ...

  2. pl/sql Developer连接oracle远程数据库

    1.下载地址:PLSQL Developer 12.1正式版64位(含中文语言包.注册码).rar   https://download.csdn.net/download/bokewangyu/11 ...

  3. 1753 -- Flip Game

    Flip Game Time Limit: 1000MS   Memory Limit: 65536K Total Submissions: 48663   Accepted: 20724 Descr ...

  4. Android系统服务 —— WMS与AMS

    “可以毫不夸张的说,Android的framework层主要是由WMS.AMS还有View所构成,这三个模块穿插交互在整个framework中,掌握了它们之间的关系和每一个逻辑步骤,你对framewo ...

  5. 基于layui的表格异步删除,ajax的简单运用

    h话不多说,看图,点击删除,出现确认框,然后点击确认删除,直接删除数据, 因为是基于面向过程的,没有用php框架写,所以有3个文件: 第一个文件:data.php:用于从数据库中获取数据 <?p ...

  6. java程序引用别的jar包打包方法

    参考文章:http://www.cnblogs.com/lanxuezaipiao/p/3291641.html 目前亲测:eclipse打包: 1.不需要手动写mainfest.inf 先利用ecl ...

  7. Selenium 2自动化测试实战39(Page Object设计模式)

    Page Object设计模式 Page object是selenium自动化测试项目开发时间的最佳设计模式之一,主要体现在对界面交互细节的封装. 1.认识page object优点如下:1.减少代码 ...

  8. java配置SSM框架下的redis缓存

    pom.xml引入依赖包 <!--jedis.jar --> <dependency> <groupId>redis.clients</groupId> ...

  9. 一百一十二:CMS系统之前台用户模型

    安装shortuuid用于前台用户的主键:pip install shortuuid 创建模型 from datetime import datetimeimport enumfrom werkzeu ...

  10. vue导航菜单调用PHP后台数据

    数据库设计: 后台PHP输出所有菜单数据(index.php): <?phpheader("Access-Control-Allow-Origin:*");header(&q ...