Java的volatile关键字用于标记一个Java变量为“在主存中存储”。更确切的说,对volatile变量的读取会从计算机的主存中读取,而不是从CPU缓存中读取,对volatile变量的写入会写入到主存中,而不只是写入到CPU缓存。

实际上,从Java5开始,volatile关键字不只是保证了volatile变量在主存中写入和读取,我回在后面的部分做相关的解释。

变量可见性问题

Java的volatile关键字保证了多个线程对变量值变化的可见性。这听起来有点抽象,让我来详细解释。

在一个多线程的程序中,当多个线程操作非volatile变量时,出于性能原因,每个线程会从主存中拷贝一份变量副本到一个CPU缓存中。如果你的计算机有多于一个CPU,每个线程可能会在不同的CPU中运行。这意味着每个简称拷贝变量到不同CPU的缓存中,如下图:

对于非volatile变量,并没有保证何时JVM从主存中读取数据到CPU缓存,或者从CPU缓存中写出数据到主存。这会导致一些问题。

想象一种情况,多于一个线程访问一个共享对象,这个共享对象包含一个计数变量如下声明:

public class ShareObject {

public int counter = 0;

}

考虑只有一个线程Thread1增加counter这个变量的值,但是Tread1和Thread2可能有时会读取counter变量。

如果counter变量没有被声明为volatile,就不能保证何时这个变量的值会从CPU缓存写回主存,这意味着,在CPU缓存中的counter变量的值可能和主存中的不一样。如下图所示:

线程没有看到一个变量最新更新的值的原因是这个变量还没有被一个线程写回到主存,这被称为“可见性”问题。一个线程对变量的更新对其他线程不可见。

Java的volatile可见性保证

Java的volatile关键字想要解决变量可见性问题。通过声明counter变量为volatile,所有对counter变量的写入都回立即写回到主存,同时所有对counter变量也都会从主存中读取。

西面的代码展示了如何把counter变量声明为volatile:

public class SharedObject {

public volatile int counter = 0;

}

声明一个变量为volatile保证了对变量的写入对其他线程的可见性。

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

但是,如果T1和T2都去增加counter变量的只,name声明counter变量为volatile是不够的,后面会说明。

全volatile可见性保证

实际上,Java的volatile的可见性保证不止volatile变量本身。可见性保证如下:

如果线程A写一个volatile变量,线程B随后读取这个volatile变量,那么在写这个volatile变量之前对线程A可见的所有变量,在线程B读取这个volatile变量之后对线程B也可见。

如果线程A读取一个volatile变量,那么当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变量,则所有对当前线程可见的变量也会都写入到主存,也就是当一个值写入到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变量中。当读取days的值时,months和years的值也被读取到主存。因此可以保证你看到的是days,months和years的最新的值,前提是保证上面的读取顺序。

指令重排序挑战

出于性能的考量,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的只在days变量修改的情况下依然会写入到主存,但是这时将years和days变量值刷入主存这件事发生在对months和years写入新值之前,则对years和days的更新对其他线程来说就不可见了。这下指令重排序就改变了程序的语义。

Java有一个应对此问题的解决方案,下面会讲到。

Java的volatile的Happens-Before保证

为了解决指令重排序的挑战,Java的volatile关键字除了可见性保证之外,给出了一个“happens-before”的保证。happens-before保证如下情况:

如果读取和写入其他非volatile变量发生在写入volatile变量之前(这种情况这些非volatile变量也会被刷入主存),则读取和写入这些变量不能被重排序为发生在写入这个volatile变量之后(禁止指令重排序)。在写入一个volatile变量之前的读取和写入非volatile变量被保证为“happen before”写入这个volatile变量。需要注意的是,例如在写入一个volatile变量之后读写其他变量可以被重排序到写入这个volatile变量之前。从“之后”重排序到”之前“是允许的,但是从”之前“重排序到”之后“是禁止的。

如果读写其他非volatile变量发生在读取一个volatile变量之后(这种情况这些非volatile变量也会被刷到主存),则读写这些变量不能被重排序为发生在读取这个volatile变量之前。需要注意的是,读取其他变量发生在读取一个volatile变量之前能够被重排序为发生在读取这个volatile变量之后。从”之前“重排序到“之后”是允许的,但是从“之后”重排序到“之前”是被禁止的。

上面的happens-before保障保证的volatile关键字的可见性是强制的。

volatile不总是足够的

尽管volatile关键字保证了所有对一个volatile变量的读取都是从主存中读取,所有对volatile关键字的写入都是直接到主存,但是仍有其他情况使得声明一个变量为volatile是不足够的。

在前面解释的情况,也就是只有Thread1写共享变量counter,声明counter变量为volatile足以保证Thread2总是看到最新写入的值。

实际上,多线程都可以写一个共享的volatile变量,并且仍然在主存中存储正确的值,前提是写入变量的新值不依赖于它之前的值。也就是说,如果一个线程写入一个值到共享的volatile变量不需要先去读它的值去产出下一个值。

只要一个线程需要首先读取一个volatile变量的值,基于这个值生成一个新值,则一个volatile关键字不足以保证正确的可见性。在读取volatile变量然后写入新值的短暂的间隙,会产生竞态条件(race condition),这时多个线程可能读取到相同的volatile变量的值,生成这个变量的新值,当将新值写回主存时,会覆盖彼此的值。

多线程增加相同计数器的值就是这种情况,导致一个volatile声明不足够。下面详细解释这种情况。

想象如果Thread1读取一个值为0的共享的counter变量到它的CPU缓存,增加1并且不将这个改变的值写回主存。Thread2然后从主存中读取相同的值仍为0counter变量到它的CPU缓存。Thread2也为它增加1,也不写回主存。这种情况如下图所示:

Thread1和Thread2此时实际上已经不同步了。共享变量counter的值应该为2,但是每个线程在CPU缓存中的这个变量的值都为1,在主存中的值仍为0,这就乱了!尽管这两个线程最终会将值写回主存中的共享变量,这个值也是不正确的。

何时volatile是足够的?

正如前面所说,如果两个线程都去读写同一个共享变量,只对这个共享变量使用volatile关键字是不够的。你需要使用一个synchronized关键字去保证读写相同变量是原子的。读写一个volatile变量不会阻塞线程的读写。

作为synchronized块替代方法,你可以使用java.util.concurrent包中的众多原子数据类型。比如,AtomicLong或者AtomicReference或其他的类型。

只有一个线程读写一个volatile变量值,其他线程只读取变量,则这些读线程能够保证看到写入这个volatile变量的最新值,如果不声明为volatile,则这种情况不能保证。

volatile的性能考量

读写volatile变量会导致变量被读写到主存。读写主存比访问CPU缓存开销更大。访问volatile变量也会禁止指令重排序,而指令重排序是一个正正常的性能优化技术。因此,你应该只在真正需要保证变量可见性的时候使用volatile变量。

java 中violate_Java中的Volatile关键字相关推荐

  1. Java中的synchronized与volatile关键字

    原文出处:http://hukai.me/android-training-course-in-chinese/performance/smp/index.html Java中的"synch ...

  2. 一文读懂Java内存模型(JMM)及volatile关键字

    点赞再看,养成习惯,公众号搜一搜[一角钱技术]关注更多原创技术文章. 本文 GitHub org_hejianhui/JavaStudy 已收录,有我的系列文章. 前言 并发编程从操作系统底层工作的整 ...

  3. 全面理解Java内存模型(JMM)及volatile关键字

    [版权申明]未经博主同意,谢绝转载!(请尊重原创,博主保留追究权) http://blog.csdn.net/javazejian/article/details/72772461 出自[zejian ...

  4. java 中violate_Java中的volatile关键字及Cache更新

    Volatile [ˈvɑːlətl],中文解释:反复无常的,易变的,不稳定的. volatile的本意是告诉编译器,此变量的值是易变的,每次读写该变量的值时务必从该变量的内存地址中读取或写入,不能为 ...

  5. 【Java线程】深入理解Volatile关键字和使用

    目录 背景 volatile原理 volatile特性 可见性 有序性 原子性 使用场景 背景 理解volatile底层原理之前,首先介绍关于缓存一致性协议的知识. 背景:计算机在执行程序时,每条指令 ...

  6. 【Java并发编程 】同步——volatile 关键字

    英 /ˈvɒlətaɪl/ 我了太噢(记不住单词怎么读) 一.volatile的介绍? volatile是一个轻量级的synchronized,一般作用与变量,在多处理器开发的过程中保证了内存的可见性 ...

  7. java(八)-线程安全、volatile关键字、原子性、并发包、死锁、线程池

    Day08[线程状态.volatile关键字.原子性.并发包.死锁.线程池] 今日目标 线程安全 volatile关键字 原子性 并发包 死锁 线程池 教学目标 能够说出volatile关键字的作用 ...

  8. Java线程详解(10)-volatile关键字

    Java 语言中的 volatile 变量可以被看作是一种 "程度较轻的 synchronized":与 synchronized 块相比,volatile 变量所需的编码较少,并 ...

  9. java中实现具有传递性吗_Java中volatile关键字详解,jvm内存模型,原子性、可见性、有序性...

    一.Java内存模型 想要理解volatile为什么能确保可见性,就要先理解Java中的内存模型是什么样的. Java内存模型规定了所有的变量都存储在主内存中.每条线程中还有自己的工作内存,线程的工作 ...

最新文章

  1. python udp编程_Python UDP编程
  2. lisp删除块中图元_DeleteBlocks
  3. 利用CSS3的transform做的动态时钟
  4. 前端经典练手项目|用 JavaScript 实现网页版扫雷
  5. XP重装后磁盘拒绝访问和无法访问加密文件夹
  6. libtorrent实现bt客户端程序
  7. 中国四大运营商2G/3G/4G/5G工作频率以及网络制式
  8. 一元函数,多元函数,可微的含义 多元函数微分的几何意义 多元函数偏导 那么为什么有微分和可导 能不能固定两个或者多个条件,多偏微分,哈哈
  9. xcode证书签名快速完美解决
  10. 不可不知的国际贸易术语
  11. 【更新21.02.03】百分浏览器继续使用Flash Player的临时解决方案!
  12. 医学图像处理涉及到的窗宽窗位 1
  13. 现代商业杂志现代商业杂志社现代商业编辑部2022年第16期目录
  14. 运行JS脚本的几种方式
  15. K2 重磅出击,构建财务共享中心方案,促进企业标准化
  16. Python基于Django城市PM2.5空气质量数据可视化分析
  17. 区块链共识协议最详细的分析
  18. 鸿蒙合香丸有副作用吗,苏合香丸能长期吃吗 有没有副作用
  19. 口红送什么色号,这是一个难题,爬取口红数据,希望对你有所帮助
  20. 如何连接成组箱线图中的平均值(seaborn)

热门文章

  1. iOS NSString 与NSData转化
  2. 算法:合并排序(Merge Sort)
  3. 【转】android通讯录列表,A~Z字母提示view
  4. linux的基础知识——多进程并发服务器
  5. 基本的Windows相关的DOS命令
  6. 检测oracle的语句,oracle功能检测sql语句
  7. python中的画布背景设置_教你用python画图—Turtle详细教程
  8. feign 整合sentinel_SpringCloud实战五-Sentinel上
  9. 数学在计算机科学上的应用文献,数学计算机论文,关于计算机在数学教学中的应用相关参考文献资料-免费论文范文...
  10. 7nmarm微架构鲲鹏服务器芯片,中国电信服务器集采:同方鲲鹏服务器拿下6000万元份额...