最近在重新梳理多线程,同步相关的知识点。关于 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 months

private 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 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;

}

}

如果我们用另一个 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 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 对此处的指令进行了重排序会发生什么?假设指令重排序后的 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 关键字。

References

volite java_如何理解 JAVA volatile 关键字相关推荐

  1. Java Volatile关键字可见性问题分析

    Java Volatile关键字可见性问题分析 Java 内容模型 普通变量(非Vola变量)的内存不可见性 Volatile变量的内存可见性 剩余疑惑 Java 内容模型 具体可以查看这篇文章Jav ...

  2. Java volatile关键字原理解剖

    Java volatile关键字原理解剖 文章目录 Java volatile关键字原理解剖 参考文章 前置知识 CPU缓存模型 CPU缓存行 并发编程基本概念 Java锁概念 volatile关键字 ...

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

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

  4. java volatile关键字的作用_java volatile关键字作用及使用场景详解

    1. volatile关键字的作用:保证了变量的可见性(visibility).被volatile关键字修饰的变量,如果值发生了变更,其他线程立马可见,避免出现脏读的现象.如以下代码片段,isShut ...

  5. Java volatile关键字详解

    1.关于volatile volatile是Java语言中的关键字,用来修饰会被多线程访问的共享变量,是JVM提供的轻量级的同步机制,相比同步代码块或者重入锁有更好的性能.它主要有两重语义,一是保证多 ...

  6. java volatile关键字使用

    1.为什么要使用volatile关键字? 先看下面的代码: //线程1 boolean stop = false; while(!stop){doSomething(); }//线程2 stop = ...

  7. Java volatile关键字总结

    JMM(为什么需要volatile) Java内存模型(JavaMemoryModel)描述了Java程序中各种变量(线程共享变量)的访问规则,以及在JVM中将变量,存储到内存和从内存中读取变量这样的 ...

  8. volite java_同步和Java内存模型(五)Volatile

    作者:Doug lea 译者:杜建雄校对者:方腾飞 Volatile 从原子性,可见性和有序性的角度分析,声明为volatile字段的作用相当于一个类通过get/set同步方法保护普通字段,如下: f ...

  9. 一个具体的例子学习Java volatile关键字

    相信大多数Java程序员都学习过volatile这个关键字的用法.百度百科上对volatile的定义: volatile是一个类型修饰符(type specifier),被设计用来修饰被不同线程访问和 ...

最新文章

  1. 无法访问linux mysql_远程无法访问linux Mysql解决方案(转)
  2. python的100个案例_Python实例100个(基于最新Python3.7版本)
  3. 进行SEPM的灾难恢复时导入数据库后,Symantec Endpoint Protection Manager(SEPM)无法登陆...
  4. ServiceStack.Ormlit 事务
  5. Replace Array with Object(以对象取代数组)
  6. Jquery一些常用的方法
  7. 入侵linux_入侵Linux计算机以获得更好的聆听体验
  8. 关于手机端适配的问题(rem,页面缩放)
  9. paip.批处理清理java项目冗余jar的方法
  10. java 中的LongAdder
  11. 维纳滤波的详细讲解(同为小白,相互取暖)
  12. php 同时登录怎么办,php 实现同一个账号同时只能一个人登录
  13. 【语音隐写】基于matlab DWT音频数字水印嵌入提取【含Matlab源码 350期】
  14. Delay-Doppler equalization(8)(时延多普勒均衡)⭐
  15. 攻击防御实例——SQL注入
  16. 使用js实现简单的注册验证
  17. docker上安装多个mysql_docker:安装mysql多个
  18. linux 内核网络中 RPS/RFS 原理Ⅰ
  19. 竹林七贤:没有信仰的人生有多可怕?
  20. CentOS下查看电脑硬件设备属性命令

热门文章

  1. Mac中安装homebrew全过程(附常用问题解决办法)
  2. 使用JVMTI实现Java源码加密及SpringBoot项目包加密解决方案实践总结
  3. 日语学习第一课日本文字
  4. oracle中的分隔函数,Oracle中的split字符串分割函数
  5. 2021年中国企业智慧通信产品研究报告
  6. cesium效果篇—雷电效果
  7. PAT1037 在霍格沃茨找零钱
  8. 如何实现一个React全家桶项目(附完整教程及代码)
  9. vivo pad2和小米平板6 pro参数对比 vivo pad2和小米平板6 pro哪个好
  10. oracle数据库账户密码重置并修改最大连接数