如何理解 JAVA 中的 volatile 关键字

最近在重新梳理多线程,同步相关的知识点。关于 volatile 关键字阅读了好多博客文章,发现质量高适合小白的不多,最终找到一篇英文的非常通俗易懂。所以学习过程中顺手翻译下来,一方面巩固知识,一方面希望能帮到有需要的伙伴。该文章并非完全逐字翻译,英文不错的可以选择阅读原文:Java Volatile Keyword

基本用法
JAVA 语言里的 volatile 关键字是用来修饰变量的,方式如下入所示。表示:该变量需要直接存储到主内存中。

public class SharedClass {public volatile int counter = 0;
}

被 volatile 关键字修饰的 int counter 变量会直接存储到主内存中。并且所有关于该变量的读操作,都会直接从主内存中读取,而不是从 CPU 缓存。(关于主内存和CPU缓存的区别,如果不理解也不用担心,下面会详细介绍)

这么做解决什么问题呢?主要是两个问题:

多线程间可见性的问题,
CPU 指令重排序的问题
注:为了描述方便,我们接下来会把 volatile 修饰的变量简称为“volatile 变量”,把没有用 volatile 修饰的变量建成为“non-volatile”变量。

理解 volatile 关键字
变量可见性问题(Variable Visibility Problem)
Volatile 可以保证变量变化在多线程间的可见性。
在一个多线程应用中,出于计算性能的考虑,每个线程默认是从主内存将该变量拷贝到线程所在CPU的缓存中,然后进行读写操作的。现在电脑基本都是多核CPU,不同的线程可能运行的不同的核上,而每个核都会有自己的缓存空间。如下图所示(图中的 CPU 1,CPU 2 大家可以直接理解成两个核):


这里存在一个问题,JVM 既不会保证什么时候把 CPU 缓存里的数据写到主内存,也不会保证什么时候从主内存读数据到 CPU 缓存。也就是说,不同 CPU 上的线程,对同一个变量可能读取到的值是不一致的,这也就是我们通常说的:线程间的不可见问题。比如下图,Thread 1 修改的 counter = 7 只在 CPU 1 的缓存内可见,Thread 2 在自己所在的 CPU 2 缓存上读取 counter 变量时,得到的变量 counter 的值依然是 0。

而 volatile 出现的用意之一,就是要解决线程间不可见性,通过 volatile 修饰的变量,都会变得线程间可见。
其解决方式就是文章开头提到的:
通过 volatile 修饰的变量,所有关于该变量的读操作,都会直接从主内存中读取,而不是 CPU 自己的缓存。而所有该变量的写操都会写到主内存上。

因为主内存是所有 CPU 共享的,理所当然即使是不同 CPU 上的线程也能看到其他线程对该变量的修改了。

volatile 不仅仅只保证 volatile 变量的可见性
volatile 在可见性上所做的工作,实际上比保证 volatile 变量的可见性更多:

当 Thread A 修改了某个被 volatile 变量 V,另一个 Thread B 立马去读该变量 V。一旦 Thread B 读取了变量 V 后,不仅仅是变量 V 对 Thread B 可见, 所有在 Thread A 修改变量 V 之前 Thread A 可见的变量,都将对 Thread B 可见。
当 Thread A 读取一个 volatile 变量 V 时,所有对于 Thread A 可见的其他变量也都会从主内存中被读取。
初次读这两句话可能会有些绕口,这里举个例子:

public class MyClass {private int years;private int monthsprivate volatile int days;public void update(int years, int months, int days){this.years  = years;this.months = months;this.days   = days;}
}

这个 MyClass 类中有一个 update 方法,会更新该类的所有3个变量:years,months,days。其中仅 days 是 volatile 变量。当 this.days = days 执行时,也就是当 days 变量的修改被写到主内存时,所有该 Thread 可见的其他变量 years,months 也都会被写到主内存中。换句话说,当 days 被修改后,years 和 months 的修改也会被其他线程可见。

再看一个关于读的例子:

public class MyClass {private int years;private int monthsprivate volatile int days;public int totalDays() {int total = this.days;total += months * 30;total += years * 365;return total;}public void update(int years, int months, int days){this.years  = years;this.months = months;this.days   = days;}
}

如果我们用另一个 Thread 调用同一个 MyClass 对象的 totalDays() ,在 int total = this.days 这一行被执行时,因为 days 是 volatile 变量,我们会从主内存中去读取 days 的值,同时,所有对于该 Thread 可见的其他变量 months 和 years 也都会从主内存中被读出。换句话说,该线程能够获取到最新的 days,months,years 的值。

以上就是关于 volatile 解决可见性问题的内容。

指令重排序挑战
出于计算性能的考虑,JVM 和 CPU 允许在保证程序语义一致的范围类,对程序内的指令进行重排序。举个例子:

int a = 1;
int b = 2;a++;
b++;

该代码在经过重排序后可能会变成:

int a = 1;
a++;int b = 2;
b++;

这两段代码的只能顺序虽然不一样,但是语义是相同的——都是定义两个变量(int a = 1 和 int b = 2),然后分别 +1。乍一看,这种重排序没有任何问题,但其实如果咱们把其中一个变量定义为 volatile 变量,此时我们再结合前面提到的可见性的延伸问题来看,大家可能会发现端倪。
还是以可见性问题中的 MyClass 类为例:

public class MyClass {private int years;private int monthsprivate volatile int days;public void update(int years, int months, int days){this.years  = years;this.months = months;this.days   = days;}
}

在可见性的部分我们说过,这里的 update() 方法中,执行到修改 days 这一行时,关于 years 和 months 的修改也会同时被写到主内存中。但如果 JVM 对此处的指令进行了重排序会发生什么?假设指令重排序后的 update() 执行过程如下:

public void update(int years, int months, int days){this.days   = days;this.months = months;this.years  = years;
}

days 的修改被提到了最前面,此时,months 和 years 的修改还没有做,换句话说,此处 months 和 years 的修改并不能保证对其他线程可见。这么一来 volatile 关于可见性保证的延伸是不是就失效了?关于这一问题我们在实际使用 volatile 时并不会碰到,因为 JAVA 已经有解决方案:Happens-Before 规则。

Java volatile Happens-Before 规则
面对指令重排序对可见性的调整,volatile 采用 Happens-Before 规则解决:

任何原始执行顺序中,在 volatile 变量写指令之前的其他变量读写指令,在重新排序后,不可以被放到 volatile 写指令之后。
所有原本就应该 volatile 变量写指令前发生的其他变量读写指令,必须依然在其之前发生(Happens-Before)。
任何原始执行顺序中,在 volatile 变量读指令之后的其他变量读写指令,在重新排序后,不可以被放到 volatile 读指令之前。
有了以上两条 Happens-Before 规则,我们就避免了指令重排序对 volatile 可见性的影响。

volatile 不能保证原子性
多线程并发中我们经常提到的“三性”:可见性,有序性,原子性。虽然 volatile 可以保证可见性,有序性,但其并不能保证原子性。
当两个线程 Thread 1 和 Thread 2 同时修改统一对象下的 volatile 变量 counter 时,比如同时执行 counter++。此时两个线程读取到的 counter 值可能都是 0,经过各线程的计算,他们认为 counter + 1 后的结果都是 1。最终虽然我们分别用两个线程对 counter 变量做了 + 1 操作,可最终结果不是 2 而是 1。因此我们说 volatile 并不能保证该变量读写操作的原子性。

如果希望避免该问题,我们需要使用 synchronized 关键字。用 synchronized 关键字来修饰我们对变量读写操作(counter++)的方法/代码块,保证该读写操作的原子性。

除了 synchronized 关键字,我们还可以直接只用 AtomicInterger 类型定义 counter 变量。AtomicInteger 提供了针对 Integer 的原子操作。类似的类还有 AtomicBoolean 和 AtomicLong。
synchronized 和 AtomicXXX 类都可以保证原子性,前者是基于锁的原理实现的原子性(悲观锁),而后者则是基于 CAS 原则(乐观锁)。

什么场景下我们只需要 volatile 就足够呢?比如:当某个变量只会被一个线程修改,其他并行线程只会执行读操作时,我们使用 volatile 就足以。

关于 Volatile 的性能问题
如果大家了解 CPU 的多级缓存机制,(不了解应该也能猜到),从主内存读取数据的效率一定比从 CPU 缓存中读取的效率低很多。包括指令重排序的目的也是为了提高计算效率,当重排序机制被限制时,计算效率也会相应收到影响。因此,我们应该只在需要保证变量可见性和有序性时,才使用 volatile 关键字。

继续支持Remi酱哦!

文章来源:https://www.jianshu.com/p/9cdbdd632625

如何理解 JAVA 中的 volatile 关键字相关推荐

  1. 深入理解Java中的volatile关键字

    在再有人问你Java内存模型是什么,就把这篇文章发给他中我们曾经介绍过,Java语言为了解决并发编程中存在的原子性.可见性和有序性问题,提供了一系列和并发处理相关的关键字,比如synchronized ...

  2. 自顶向下彻底理解 Java 中的 volatile 关键字

    标题 neta 自<计算机网络自顶向下> 思维导图 volatile 在 Java 中被称为轻量级 synchronized.很多并发专家引导用户远离 volatile 变量,因为使用它们 ...

  3. 深入理解Java中的final关键字

    深入理解Java中的final关键字 http://www.importnew.com/7553.html Java中的final关键字非常重要,它可以应用于类.方法以及变量.这篇文章中我将带你看看什 ...

  4. java中的Volatile关键字使用

    文章目录 什么时候使用volatile Happens-Before java中的Volatile关键字使用 在本文中,我们会介绍java中的一个关键字volatile. volatile的中文意思是 ...

  5. Java中的Volatile如何工作? Java中的volatile关键字示例

    如何在Java中使用Volatile关键字 在Java采访中,什么是volatile变量以及何时在Java中使用volatile变量是Java 采访中一个著名的多线程采访问题 . 尽管许多程序员都知道 ...

  6. (转)深入理解Java中的final关键字

    转自:http://www.importnew.com/7553.html Java中的final关键字非常重要,它可以应用于类.方法以及变量.这篇文章中我将带你看看什么是final关键字?将变量,方 ...

  7. java final 变量 好处_深入理解Java中的final关键字

    Java中的final关键字非常重要,它可以应用于类.方法以及变量.这篇文章中我将带你看看什么是final关键字?将变量,方法和类声明为final代表了什么?使用final的好处是什么?最后也有一些使 ...

  8. 10074---深入理解Java中的final关键字

    原文 Java中的final关键字非常重要,它可以应用于类.方法以及变量.这篇文章中我将带你看看什么是final关键字?将变量,方法和类声明为final代表了什么?使用final的好处是什么?最后也有 ...

  9. java中final是啥意思_(转)深入理解Java中的final关键字

    Java中的final关键字非常重要,它可以应用于类.方法以及变量.这篇文章中我将带你看看什么是final关键字?将变量,方法和类声明为final代表了什么?使用final的好处是什么?最后也有一些使 ...

最新文章

  1. 德国图宾根大学发布可扩展「对抗黑盒攻击」,仅通过观察决策即可愚弄深度神经网络
  2. Java 单例设计模式
  3. (hdu step 7.2.1)The Euler function(欧拉函数模板题——求phi[a]到phi[b]的和)
  4. C++ Bitsets
  5. CS229 6.5 Neurons Networks Implements of Sparse Autoencoder
  6. php signature解密,openssl RSA非对称加密、解密、签名、验签
  7. 移动端性能优化系列—启动速度
  8. 吴思涵国内首场肿瘤ecDNA学术报告|深度揭秘半个世纪ecDNA的研究成果及突破性进展...
  9. 如何编写完美的 Python 命令行程序?
  10. SQL 基础笔试题 (三)
  11. Prefer copy Over retain
  12. Unable to find setter method for attribute: [commandName]
  13. 程序员必备的8个学习工具
  14. 显示屏色温调节 影响 测试软件,Twilight屏幕色温调节软件
  15. 【20220318】执行脚本提示killed
  16. ios 中字符串怎么换行
  17. .bat文件打开闪退
  18. 易經大意(12) 三和 韓長庚 著
  19. 高级算法梳理之LightGBM
  20. 浅谈CGI程序与web间数据交互

热门文章

  1. 通过SUBMIT获取ALV GRID的数据
  2. OO实现ALV TABLE 十一:ALV的聚合,排序,过滤
  3. 关于SAP物料的历史库存
  4. 广告植入、电商导流……“变味”的线上演唱会
  5. 计算机专业英语在线测试9,《计算机专业英语》第9章在线测试【DOC精选】.doc
  6. 使用jdbc操作mysql数据库_java使用JDBC操作MYSQL数据库
  7. js手机端时间区间_js 时间区间
  8. python输出杨辉三角啊二维数组_Java输出杨辉三角形(使用二维数组)
  9. java中配置bean_Spring中基于Java的配置@Configuration和@Bean用法
  10. hive substr函数_数据分析工具篇——HQL函数及逻辑