翻译:Java volatile 关键词

xindoo 2021-01-22 11:54:48
java 翻译 volatile 关键 关键词


Java中的volatile关键词被用来将变量标记为“存储在内存中”。准确地的讲每次volatile变量的读取和写入都是直接操作内存,而不是cpu cache。 实际上自从java 5之后,volatile关键词保证除了volatile变量直接读写内存外,它也被赋予了更多的含义,文章后续会解释。

变量可见性问题

java volatile 关键词保证变量在多线程间变化的可见性。听起来有点抽闲,让我详细说明下。

在多线程应用中,当线程操作非volatile变量时,因为性能上的考虑,每个线程会把变量从内存中拷贝一份到cpu cache里(译者注:读写一次磁盘需要100ns,level1 cache只需要1ns)。如果你的电脑有多个cpu,每个线程运行在不同的cpu上,这就意味着每个线程会将变量拷贝到不同的cpu cache上,如下图。

对于非volatile变量,JVM不会保证每次都写都是从内存中读写,这可能会导致一系列的问题。

试想下这样一个场景,多个线程操作一个包含计数器的变量,如下。

public class SharedObject {
public int counter = 0;
}

如果只有线程1会增加counter,但线程1和线程2会时不时读counter。

如果counter没有被声明成volatile,jvm不会保证每次counter写cpu cache都会被同步到主存。这就意味着cpu cache里的数据和主存中的有可能不一致,如下图所示。

另一个线程线程没法读到最新的变量值,因为数据还没有从cpu cache里同步到主存中,这就是 可见性问题,数据的更新对其他线程不可见。

Java volatile可见性保证

Java volatile的诞生就是为了解决可见性问题。把counter变量声明为volatile,所有counter的写入都会被立刻写会到主存里,所有的读都会从主存里直接读。

volatile的用法如下:

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

把变量声明为volatile保证了其他线程对这个变量更新的可见性。 在上面的例子中,线程1修改counter变量,线程2只读不修改,把counter声明为volatile就可以保证线程2读取数据的正确性了。 当时,如果线程1线程2都会修改这个变量,那volatile也无法保证数据的准确性了,后续会详解。

volatile 完全可见性保证

实际上,Java volatile可见性保证超出了volatile变量本身。可见性保证如下。 - 如果线程A修改一个volatile变量,并且线程B随后读取了同一个变量,你们线程A在写volatile变量前的所有变量操作在线程B读取volatile变化后对线程B都可见。 - 如果线程A读取了volatile变量,那么在它之后线程A读取都所有变量都将从主存中重新读取。 测试代码如下:

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

update()方法写了三个变量,但只有days被声明为volatile。 volatile完全可见性保证的含义是:当线程修改了days,所有的变量都会被同步到主存中,在这里years和months也会被同步到主存中。

在读取years、months、days的时候,你可以这么做。

public class MyClass {
private int years;
private int months
private 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;
}
}

当调用totalDays()方法后,当读取days之后到total变量后,months和years也会从主存中同步。如果按上面的顺序,可以保证你一定读到days,months和years的最新值。

译者注:在上面这个特定读写顺序下,虽然只有days是volatile变量,但days和months也实现了volatile。我猜测原因和cpu硬件有关,volatile变量读取前将要读取的地址在cpu cache中置为失效,这样就保证了每次读取前必须从内存中做数据更新。同样写入后会强制同步cache数据到主存中,这样就实现了volatile语义。但实际上cpu cache在管理cache数据的时候并不是以单个地址为单位,而是以一个block为单位,所以一个block中只要有一个volatile变量,那么读写这个变量都会导致整个block和主存同步。 综上所述,我认为原作者博客中这部分内容不具备参考性,java没有承诺过类似的保证,而且这种可见性估计和具体的cpu实现有关,可能不具备可迁移性,不建议大家这么用。所以如果有多个变量需要可见性保证,还是得都加volatile标识。

指令重排序挑战

Jvm和cpu为性能考虑都可能会最大指令进行重排序,但都会保证语义的一致性。例如:

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

这些指令在保证语义正确性下可以被重排为下面的次序。

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

但当一个变量是volatile的时候,指令重排序会面临一个挑战。 让我们再来看下上面提到的MyClass()的例子。

public class MyClass {
private int years;
private int months
private 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像下面一样重新排列了指令:

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

虽然months和years最终也会被写入到主存中,但却不是实时的,无法保证对其他线程的立即可见。实际语义也会因为指令重排序而改变。 Java 实际上已经解决了这个问题,让我们接着看下去。

Java volatile和有序性(Happens-Before)保证

为了解决重排序的挑战,java volatile关键词可见性之上也保证了"有序性(happens-before)",有序性的保证含义如下。 - 对其他变量的读和写如果原来就在volatile变量写之前,就不能重排到volatile变量的写之后。 一个volatile变量写之前的的读/写保证在写之前。请注意有特殊情况,例如,对volatile的写操作之后的其他变量的读/写操作会在对volatile的写操作之前重新排序。而不是反过来。从后到前是允许的,但从前到后是不允许的。 - 如果对其他变量的读/写如果最初就在对volatile变量的读/写之后,则不能将其重排序到volatile读之前。请注意,在读取volatile变量之前发生的其他变量的读取可以在读取volatile变量之后重新排序。而不是反过来。从前到后是允许的,但从后到前是不允许的。

上面的happens-before保证确保了volatile关键字强可见性。

volatile还不够

尽管volatile保证数据的读写都是从主存中直接操作的,但还有好多情况下volatile语义还是不够的。在前面的例子中,线程1写counter变量,如果将counter声明为volatile,线程2总能看到最新的值。

但事实上,如果多个线程都可以写共享的volatile变量且每次写入的新值不依赖于旧值,依旧可以保证变量值的准确性,换句话说就是有个线程写之前不需要先读一次再在读入的数据上计算出下一个值。

在读出-计算-写入的模式下就无法再保证数值的正确性了,因为在计算的过程中,这个时间差下数据可能已经被其他线程更新过了,多个线程可能竞争写入数据,就会产生数据覆盖的情况。

所以在上面例子中,多个线程共同更新counter的情况下,volatile就无法保证counter数值的准确性了。下面会详细解释这种情况。

想象下线程1读到的counter变量是0,然后它加了1,但没有写回到主存里,而是写在cpu cache里。这个时候线程2同样也看到的counter是0,它也同样加了1并只写到自己的cpu cache中,就像下图这样。

这个时候线程1和线程2的数据实际上是不同步的。我们预期的counter实际值应该是2,但在主存中是0,在某个cpu cache中是1。最终某个线程cpu cache中的数据会同步会主存,但数据是错的。

什么时候volatile就足够了?

像上文中提到的一样,如果有多个线程都读写volatile变量,只用volatile远远不够,你需要用synchronized来保证读和写是一个原子操作。 读和写一个volatile变量不会阻塞其他的线程,为了避免这种情况发生,你必须使用synchronized关键词。

除了synchronized之外,你还可以使用java.util.concurrent包中提供的原子数据类型,比如AtomicLong或者AtomicReferences。 如果只有一个线程写入,其他线程都是只读,那么volatile就足够了,但不使用volatile的话是不能保证数据可见性的。

注意:volatile只保证32位和64位变量的可见性。

volatile的性能考量

volatile会导致数据的读写都直接操作主存,读写主存要不读写cpu cache慢的多。volatile也禁止了指令重排序,指令重排序是常见的性能优化手段,所以你应该只在真正需要强制变量可见性时才使用volatile。

原文地址

http://tutorials.jenkov.com/java-concurrency/volatile.html

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

版权声明
本文为[xindoo]所创,转载请带上原文链接,感谢
https://cloud.tencent.com/developer/article/1778260

  1. 【计算机网络 12(1),尚学堂马士兵Java视频教程
  2. 【程序猿历程,史上最全的Java面试题集锦在这里
  3. 【程序猿历程(1),Javaweb视频教程百度云
  4. Notes on MySQL 45 lectures (1-7)
  5. [computer network 12 (1), Shang Xuetang Ma soldier java video tutorial
  6. The most complete collection of Java interview questions in history is here
  7. [process of program ape (1), JavaWeb video tutorial, baidu cloud
  8. Notes on MySQL 45 lectures (1-7)
  9. 精进 Spring Boot 03:Spring Boot 的配置文件和配置管理,以及用三种方式读取配置文件
  10. Refined spring boot 03: spring boot configuration files and configuration management, and reading configuration files in three ways
  11. 精进 Spring Boot 03:Spring Boot 的配置文件和配置管理,以及用三种方式读取配置文件
  12. Refined spring boot 03: spring boot configuration files and configuration management, and reading configuration files in three ways
  13. 【递归,Java传智播客笔记
  14. [recursion, Java intelligence podcast notes
  15. [adhere to painting for 386 days] the beginning of spring of 24 solar terms
  16. K8S系列第八篇(Service、EndPoints以及高可用kubeadm部署)
  17. K8s Series Part 8 (service, endpoints and high availability kubeadm deployment)
  18. 【重识 HTML (3),350道Java面试真题分享
  19. 【重识 HTML (2),Java并发编程必会的多线程你竟然还不会
  20. 【重识 HTML (1),二本Java小菜鸟4面字节跳动被秒成渣渣
  21. [re recognize HTML (3) and share 350 real Java interview questions
  22. [re recognize HTML (2). Multithreading is a must for Java Concurrent Programming. How dare you not
  23. [re recognize HTML (1), two Java rookies' 4-sided bytes beat and become slag in seconds
  24. 造轮子系列之RPC 1:如何从零开始开发RPC框架
  25. RPC 1: how to develop RPC framework from scratch
  26. 造轮子系列之RPC 1:如何从零开始开发RPC框架
  27. RPC 1: how to develop RPC framework from scratch
  28. 一次性捋清楚吧,对乱糟糟的,Spring事务扩展机制
  29. 一文彻底弄懂如何选择抽象类还是接口,连续四年百度Java岗必问面试题
  30. Redis常用命令
  31. 一双拖鞋引发的血案,狂神说Java系列笔记
  32. 一、mysql基础安装
  33. 一位程序员的独白:尽管我一生坎坷,Java框架面试基础
  34. Clear it all at once. For the messy, spring transaction extension mechanism
  35. A thorough understanding of how to choose abstract classes or interfaces, baidu Java post must ask interview questions for four consecutive years
  36. Redis common commands
  37. A pair of slippers triggered the murder, crazy God said java series notes
  38. 1、 MySQL basic installation
  39. Monologue of a programmer: despite my ups and downs in my life, Java framework is the foundation of interview
  40. 【大厂面试】三面三问Spring循环依赖,请一定要把这篇看完(建议收藏)
  41. 一线互联网企业中,springboot入门项目
  42. 一篇文带你入门SSM框架Spring开发,帮你快速拿Offer
  43. 【面试资料】Java全集、微服务、大数据、数据结构与算法、机器学习知识最全总结,283页pdf
  44. 【leetcode刷题】24.数组中重复的数字——Java版
  45. 【leetcode刷题】23.对称二叉树——Java版
  46. 【leetcode刷题】22.二叉树的中序遍历——Java版
  47. 【leetcode刷题】21.三数之和——Java版
  48. 【leetcode刷题】20.最长回文子串——Java版
  49. 【leetcode刷题】19.回文链表——Java版
  50. 【leetcode刷题】18.反转链表——Java版
  51. 【leetcode刷题】17.相交链表——Java&python版
  52. 【leetcode刷题】16.环形链表——Java版
  53. 【leetcode刷题】15.汉明距离——Java版
  54. 【leetcode刷题】14.找到所有数组中消失的数字——Java版
  55. 【leetcode刷题】13.比特位计数——Java版
  56. oracle控制用户权限命令
  57. 三年Java开发,继阿里,鲁班二期Java架构师
  58. Oracle必须要启动的服务
  59. 万字长文!深入剖析HashMap,Java基础笔试题大全带答案
  60. 一问Kafka就心慌?我却凭着这份,图灵学院vip课程百度云