文档章节

Java Volatile 关键字详解

码代码的小司机
 码代码的小司机
发布于 2017/09/12 08:10
字数 2856
阅读 11
收藏 0
点赞 0
评论 0

本文翻译自 Java Volatile Keyword

Java关键字volatile用于将一个Java变量标记为 在主内中存储 ,更准确的解释为:每次读取一个volatile变量时将从电脑的主内存中读取而不是从CPU缓存中读取,每次对一个volatile变量进行写操作时,将会写入到主内存中而不是写入到CPU缓存中。

事实上,从Java5之后,volatile关键字不仅仅可以用来确保volatile变量是写入到主内存和从主内存读取数据,我会在下面的章节进行详细的介绍:

Volatile变量可见性保证

Java volatile关键字确保了volatile变量的修改在多线程中是可见的。这听起来有些抽象,接下来我将详细说明。

在一个对非volatile变量进行操作的多线程应用,由于性能的关系,当对这些变量进行读写时,每个线程都可能从主线程中拷贝变量到CPU缓存中。如果你的电脑不止一个CPU,每个线程可能会在不同的CPU上运行。这意味着,每个线程都可能将变量拷贝到不同的CPU的CPU缓存中,如下图所示: p1.png 
对于volatile变量而言,Java虚拟机(JVM)不能确保什么时候将数据从主内存读取到CPU缓存以及什么时候将CPU缓存的数据写入到主内存中。而这可能会引起一些问题,我将稍后解释。

假设两个或更多的线程对下面这个包含一个计数器的共享变量拥有访问权限:

public class SharedObject {
    public int counter = 0;
}

再次假设,只有Thread1会增加 counter 变量的值,但是Thread1和Thread2都能在任意时刻读取 counter 变量的值。

如果 couner 变量没有声明为volatile将无法保证在何时把CPU缓存中的值写入主内存中。这意味着 counter 变量在CPU缓存中的值可能会与主内存中的值不一样,如下所示:
p2.png 
造成线程不能获取变量最新值得原因为变量值没有被其它线程及时写回主内存中,这就是所谓的可见性问题。某个线程的更新对其它线程不可见。

将 counter 变量声明为volatile之后,所有对 counter 变量的写操作会立即写入主内存中,同样,所有对 counter 变量的读操作都会从主内存中读取数据。下面的代码块展示了如何将 counter 变量声明为volatile

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

因此定义一个volatile变量可以保证写变量的操作对于其它线程可见。

Volatile先行发生原则

从Java5之后volatile关键字不仅能用于确保变量从主内存中读取和写入,事实上,volatile关键字还有如下作用:

  • 如果线程A写入了一个volatile变量然后线程B读取了这个相同的volatile变量,那么所有在线程A写之前对其可见的变量,在线程B读取这个volatile之后也会对其可见。
  • volatile变量的读写指令不能被JVM重排序(出于性能的考虑,JVM可能会对指令重排序如果JVM检测到指令排序不会对程序运行产生变化)。 前后的指令可以重排序,但是volatile变量的读和写不能与这些重排序指令混在一起。任何跟随在volatile变量读写之后的指令都会确保只有在变量的读写操作之后才能执行。

上述说明需要更进一步的解释。

当一个线程向一个volatile变量写操作,此时不仅这个volatile变量自身会写入主内存,所有这个volatile变量写入之前受影响发生改变的变量也会刷写入主内存。当一个线程向一个volatile变量读操作时它同样也会从主内存中读取所有和这个volatile变量一起刷写入主内存的变量。

看看下面这个示例:

Thread A:
    sharedObject.nonVolatile = 123;
    sharedObject.counter     = sharedObject.counter + 1;

Thread B:
    int counter     = sharedObject.counter;
    int nonVolatile = sharedObject.nonVolatile;

由于线程A在写操作volatile变量 sharedObject.counter 之前写操作非volatile变量 sharedObject.nonVolatile ,因而当线程A写操作变量 sharedObject.counter 后,变量 sharedObject.nonVolatile 和 sharedObject.counter 都被写入主内存。

由于线程B以读取volatile变量 sharedObject.counter 开始,因而变量 sharedObject.counter 和变量sharedObject.nonVolatile都会被写入线程B所使用的CPU缓存中。当线程B读取 sharedObject.nonVolatile 变量时,它将能看见被线程A写入的变量。

开发人员可以利用这个扩展的可见性来优化线程之间变量的可见性。不同于把每个变量都设置为volatile,此时只有少部分变量需要声明为volatile。下面是一个利用此规则编写的简单示例程序 Exchanger :

public class Exchanger {

    private Object   object       = null;
    private volatile hasNewObject = false;

    public void put(Object newObject) {
        while(hasNewObject) {
            //等待,不覆盖已经存在的新对象
        }
        object = newObject;
        hasNewObject = true; //volatile写入
    }

    public Object take(){
        while(!hasNewObject){ //volatile读取
            //等待,不获取旧的对象(或null对象)
        }
        Object obj = object;
        hasNewObject = false; //volatile写入
        return obj;
    }
}

线程A随时可能会通过调用 put() 方法增加对象,线程B随时可能会通过调用 take() 方法获取对象。只要线程A只调用 put(),线程B只调用 take() ,这个 Exchanger 就可以通过一个volatile变量正常工作(排除synchronized代码块的使用)。

然而,JVM可能会重排序Java指令来优化性能,如果JVM可以通过不改变这些重排序指令的语义来实现此功能。如果JVM调换了 put() 和 take() 中的读和写的指令,会发生什么呢?如果 put() 真的像下面这样执行会出现什么情况呢?

while(hasNewObject) {
    //等待,不覆盖已经存在的新对象
}
hasNewObject = true; //volatile写入
object = newObject;

请注意此时对于volatile变量 hasNewObject 的写操作会在新变量的实际设置前先执行,而这在JVM看来可能会完全合法。两个写操作指令的值不再依赖于对方。

但是,对于执行指令重排序可能会损害 object 变量的可见性。首先,线程B可能会在线程A对 object 真实的写入一个值到object之前读取到 hasNewObject 的值为true。其次,现在甚至不能保证什么时候写入 object 的新值会刷写入主内存(好吧,下次线程A在其它地方写入volatile变量。。。)

为了阻止上面所述的这种情况发生,volatile关键字提供了一个 先行发生原则。先行发生保证确保对于volatile变量的读写指令不会被重排序。程序运行中前后的指令可能会被重排序,但是volatile读写指令不能和它前后的任何指令重新排序。

看看下面这个例子:

sharedObject.nonVolatile1 = 123;
sharedObject.nonVolatile2 = 456;
sharedObject.nonVolatile3 = 789;

sharedObject.volatile     = true; //a volatile variable

int someValue1 = sharedObject.nonVolatile4;
int someValue2 = sharedObject.nonVolatile5;
int someValue3 = sharedObject.nonVolatile6;

JVM可能会重新排序前3条指令,只要它们都先发生于volatile写指令(它们都必须在volatile写指令之前执行)。

同样的,JVM可能会重新排序最后3条指令,只要volatile写指令先行发生于它们,这3条指令都不能被重新排序到volatile指令的前面。

这就是volatile先行发生原则的基本含义。

Volatile并不是万能的

尽管volatile关键字确保了所有对于volatile变量的读操作都是直接从主内存中读取的,所有对于volatile变量的写操作都是直接写入主内存的,但仍有一些情况只定义一个volatile变量是不够的。

在前面的场景中,线程1对共享变量counter写入操作,声明 counter 变量为volatile之后就能够确保线程2总是可以看见最新的写入值。

事实上,如果写入该变量的值不依赖于它前面的值,多个线程甚至可以在写入一个共享的volatile变量时仍然能够持有在主内存中存储的正确值。换句话解释为,如果一个线程在写入volatile共享变量时,不需要先读取该变量的值以计算下一个值。

一旦一个线程需要首先读取一个volatile变量的值,然后基于该值产生volatile共享变量的下一个值,那么该volatile变量将不再能够完全确保正确的可见性。在读取volatile变量和写入它的新值这个很短的时间间隔内,产生了一个 竞争条件 :多个线程可能会读取volatile变量的相同值,然后产生新值并写入主内存,这样将会覆盖互相的值。

这种多个线程同时增加相同计数器的场景正是volatile变量不适用的地方,接下来的部分进行了更详细的解释。

假设线程1读取一个值为0的共享变量 counter 到它的CPU缓存中,将它加1但是并没有将增加后的值写入主内存中。线程2可能会从主内存中读取同一个 counter 变量,其值仍然为0,同样不将其写入主内存中,就如下面的图片所展示的那样:
p3.png

线程1和线程2现在都没有同步,共享变量 counter 的真实值应该是2,但是在每个线程的CPU缓存中,其值都为1,并且主内存中的值仍然是0。它成了一个烂摊子,即使这些线程终于它们对共享变量 counter 的计算值写入到主内存中,counter的值仍然是错的。

Volatile的适用场景

就如在前面提到的那样,如果两个线程同时对一个共享变量进行读和写,那么仅用volatile变量是不够的。在这种情况下,你需要使用synchronized来确保关于该变量的读和写都是原子操作。读或写一个volatile变量时并不会阻塞其它线程对该变量的读和写。在这种情况下必须用synchronzied关键字来修饰你的关键代码。

除了使用synchronzied之外,你也可以使用 java.util.concurrent 包中的一些原子数据类型,如 AtomicLong , AtomicReference 等。

当只有一个线程对一个volatile变量进行读写而其它线程只读取该变量时,volatile可以确保这些读线程读取到的是该变量的最新写入值。如果不声明该变量为volatile,则不能这些读线程保证读取的是最新写入值。

Volatile关键字适用于32位变量和64位变量。

Volatile性能思考

由于volatile变量的读和写都是直接从主内存中进行的,相对于CPU缓存,直接对主内存进行读写代价更高, 访问一个volatile变量也会阻止指令重新排序,而指令排序也是一个常用的性能增强技术。因此,你应该在只有当你确实需要确保变量可见性的时候才使用volatile变量。

© 著作权归作者所有

共有 人打赏支持
码代码的小司机
粉丝 21
博文 74
码字总数 41277
作品 0
杭州
高级程序员
Java volitile关键字详解

郑重说明 根据https://www.cnblogs.com/dolphin0520/p/3920373.html改编,做了适当删减。 1.背景 在Java 5之前,它是一个备受争议的关键字,因为在程序中使用它往往会导致出人意料的结果。在...

u010321471
04/25
0
0
Java并发学习之Volatile及内存模型探究

volatile工作原理 java编程语言允许线程访问共享变量,为了确保共享变量能被准确和一致的更新,线程应该确保通过排他锁单独获得这个变量。 Java语言提供了volatile,在某些情况下比锁更加方便...

小灰灰Blog
2017/11/02
0
0
Java学习资料-标识符、关键字

1、标识符 (1)Java对各种变量、方法和类等要素命名时使用的字符序列称为标识符。凡是自己可以起名字的地方都叫标识符,都遵守标识符的规则。 (2)Java标识符命名规则: 标识符由字母、下划...

晓阳
2015/01/05
0
0
java多线程详解一线程的内存模型和线程特性

这几天面试互联网公司的高级java工程师,多线程问的相对而言比较多。所以,从各种角度来看看java中多线程的实现方式。 一.Java多线程中的内存模型 1.java主内存和工作内存 根据java内存模型,...

onedotdot
07/14
0
0
13、Java并发性和多线程-Java Volatile关键字

以下内容转自http://tutorials.jenkov.com/java-concurrency/volatile.html(使用谷歌翻译): Java 关键字用于将Java变量标记为“存储在主存储器”中。更准确地说,这意味着,每个读取volat...

easonjim
2017/06/16
0
0
Java中的transient,volatile和strictfp关键字

Java中的transient,volatile和strictfp关键字 如果用transient声明一个实例变量,当对象存储时,它的值不需要维持。例如: class T { transient int a; //不需要维持 int b; //需要维持 } ...

xiahuawuyu
2012/05/17
0
0
Java中的volatile关键字-转载

关于volatile,我们知道,在Java中设置变量值的操作,除了long和double类型的变量外都是原子操作,也就是说,对于变量值的简单读写操作没有必要进行同步。这在JVM 1.2之前,Java的内存模型实...

LiangX
2012/01/31
0
0
Java中的transient,volatile和strictfp关键字

class T { transient int a; //不需要维持 int b; //需要维持 } class T {transient int a; //不需要维持int b; //需要维持} 这里,如果T类的一个对象写入一个持久的存储区域,a的内容不被保...

哈全文
2012/09/03
0
0
关于Java里面多线程同步的一些知识

# 关于Java里面多线程同步的一些知识 对于任何Java开发者来说多线程和同步是一个非常重要的话题。比较好的掌握同步和线程安全相关的知识将使得我们则更加有优势,同时这些知识并不是非常容易...

欧阳海阳
07/13
0
0
Java中volatile关键字的作用

volatile是Java中用来做同步的一个关键字,之前对它的作用一直理解得不是很透彻。 于是在网上查阅了一些资料,发现也讲得含混不清。 后来在wikipedia(http://en.wikipedia.org/wiki/Volatile...

GreenDay
2014/03/08
0
0

没有更多内容

加载失败,请刷新页面

加载更多

下一页

SpringBoot | 第十章:Swagger2的集成和使用

前言 前一章节介绍了mybatisPlus的集成和简单使用,本章节开始接着上一章节的用户表,进行Swagger2的集成。现在都奉行前后端分离开发和微服务大行其道,分微服务及前后端分离后,前后端开发的...

oKong
今天
2
0
Python 最小二乘法 拟合 二次曲线

Python 二次拟合 随机生成数据,并且加上噪声干扰 构造需要拟合的函数形式,使用最小二乘法进行拟合 输出拟合后的参数 将拟合后的函数与原始数据绘图后进行对比 import numpy as npimport...

阿豪boy
今天
1
0
云拿 无人便利店

附近(上海市-航南路)开了家无人便利店.特意进去体验了一下.下面把自己看到的跟大家分享下. 经得现场工作人员同意后拍了几张照片.从外面看是这样.店门口的指导里强调:不要一次扫码多个人进入....

周翔
昨天
1
0
Java设计模式学习之工厂模式

在Java(或者叫做面向对象语言)的世界中,工厂模式被广泛应用于项目中,也许你并没有听说过,不过也许你已经在使用了。 简单来说,工厂模式的出现源于增加程序序的可扩展性,降低耦合度。之...

路小磊
昨天
158
1
npm profile 新功能介绍

转载地址 npm profile 新功能介绍 npm新版本新推来一个功能,npm profile,这个可以更改自己简介信息的命令,以后可以不用去登录网站来修改自己的简介了 具体的这个功能的支持大概是在6这个版...

durban
昨天
1
0
Serial2Ethernet Bi-redirection

Serial Tool Serial Tool is a utility for developing serial communications, custom protocols or device testing. You can set up bytes to send accordingly to your protocol and save......

zungyiu
昨天
1
0
python里求解物理学上的双弹簧质能系统

物理的模型如下: 在这个系统里有两个物体,它们的质量分别是m1和m2,被两个弹簧连接在一起,伸缩系统为k1和k2,左端固定。假定没有外力时,两个弹簧的长度为L1和L2。 由于两物体有重力,那么...

wangxuwei
昨天
0
0
apolloxlua 介绍

##项目介绍 apolloxlua 目前支持javascript到lua的翻译。可以在openresty和luajit里使用。这个工具分为两种模式, 一种是web模式,可以通过网页使用。另外一种是tool模式, 通常作为大规模翻...

钟元OSS
昨天
2
0
Mybatis入门

简介: 定义:Mybatis是一个支持普通SQL查询、存储过程和高级映射的持久层框架。 途径:MyBatis通过XML文件或者注解的形式配置映射,实现数据库查询。 特性:动态SQL语句。 文件结构:Mybat...

霍淇滨
昨天
2
0
开发技术瓶颈期,如何突破

前言 读书、学习的那些事情,以前我也陆续叨叨了不少,但总觉得 “学习方法” 就是一个永远在路上的话题。个人的能力、经验积累与习惯方法不尽相同,而且一篇文章甚至一本书都很难将学习方法...

_小迷糊
昨天
1
0

没有更多内容

加载失败,请刷新页面

加载更多

下一页

返回顶部
顶部