Java Volatile Keyword

Java Volatile Keyword这几天学习Java内存模型,查看文章:JSR133(JavaMemoryModel)FAQ里面介绍了新的Java内存模型对volatile关键字的修订,因为只是一个FAQ,并没有很详细的解析volatile关键字的用法,找到一篇文章JavaVolatileKeyword详细的介绍了volatile适用的场景以及不适用的场景,翻译一下主要内容:…

大家好,又见面了,我是你们的朋友全栈君。

这几天学习 Java 内存模型,查看文章:JSR 133 (Java Memory Model) FAQ

里面介绍了新的 Java 内存模型对 volatile 关键字的修订,因为只是一个 FAQ,并没有很详细的解析 volatile 关键字的用法,找到一篇文章

Java Volatile Keyword

详细的介绍了 volatile 适用的场景以及不适用的场景,翻译一下


主要内容:

  1. 引言
  2. 变量可见性问题(Variable Visibiltiy Problems
  3. Java volatile 可见性保证(The Java volatile Visibility Guarantee
  4. 指令重排序挑战(Instruction Reordering Challenges
  5. Java volatile Happens-before 保证(The Java volatile Happens-before Guarantee
  6. volatile 并不总是足够(volatile is Not Always Enough
  7. 什么情况下 volatile 就足够了?(When is volatile Enough?
  8. volatile 性能考虑(Performance Considerations of volatile


引言

Java 关键字 volatile 被用于标记一个 Java 变量,表示 “存储在主内存”。更精确的说,对 volatile 变量的每一次读都来自于主内存而不是缓存;对 volatile 变量的每一次写也将会刷新到主内存,而不是保存在缓存中。

事实上,自从 Java 5 以来,volatile 关键字有了更多的作用,不仅仅是对 volatile 变量的读写都来自内存。我将在接下来的章节介绍。



变量可见性问题

Java volatile 关键字保证了线程之间对变量改变的可见性。这听起来有点抽象,让我来详细说明。

在多线程应用中,因为性能关系,线程操作的非 volatile 变量都会从主内存复制到缓存中。如果是多 CPU 机器,那么每个线程运行在不同的 CPU 上。这样的话,每个线程都会复制变量到不同的 CPU 缓存中,如下图所示:


这里写图片描述

volatile 变量无法保证 JVM 何时从主内存读数据或者何时将缓存数据刷新回主内存。这将造成下面几个问题。

想象这种场景,多个线程访问同一个共享对象,该对象包含了一个计数器变量:

public class SharedObject {
    public int counter = 0;
}

如果只有线程 1 会对变量 counter 执行增量操作,线程 1 和 线程 2 偶尔会读取变量 counter

如果 counter 没有被定义为 volatile,那么无法保证写入的 counter 变量从缓存刷新回内存。这也意味着,缓存中的 counter 变量的值和主内存中的不一样。如下图所示:


这里写图片描述

因为变量的最新值没有被写入主内存,导致线程无法看见变量的最新值的问题称为可见性问题 – 即一个线程的更新对其它线程不可见。



Java volatile 可见性保证

Java volatile 关键字旨在解决变量可见性问题。通过声明变量 countervolatile,所有写入 counter 变量的数据将马上被刷新到主内存。同样的,所有读取 counter 变量的数据都来自主内存。

修改后的代码如下:

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

声明变量为 volatile,因此保证了线程对变量的可见性。

在上面的场景中,线程 T1 修改计数器然后线程 T2 读取计数器,声明 counter 变量为 volatile 足够保证 T2 对写入 counter 变量数据的可见性。

然而,如果 T1T2 同时对 counter 变量进行增量操作,那么仅仅声明 counter 变量为 volatile 是不够的。

完整的 volatile 可见性保证(Full volatile Visibility Guarantee

事实上,Java volatile 的可见性保证超出了 volatile 变量本身。可见性保证如下:

  1. 如果线程 A 写入变量 volatile,随后线程 B 读取相同的 volatile 变量,那么在写入 volatile 变量之前对线程 A 可见的变量,同样对读取 volatile 变量后的线程 B 可见;(If Thread A writes to a volatile variable and Thread B subsequently reads the same volatile variable, then all variables visible to Thread A before writing the volatile variable, will also be visible to Thread B after it has read the volatile variable.
  2. 如果线程 A 读取一个 volatile 变量,那么对线程 A 可见的变量在读取 volatile 变量的时候重新从主内存读取。(If Thread A reads a volatile variable, then all all variables visible to Thread A when reading the volatile variable will also be re-read from main memory.

通过一段代码验证:

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 更新了 3 个变量,其中变量 daysvolatile 的。

完整的 volatile 可见性保证的含义是当 days 写入一个值,那么所有对这个线程可见的变量都将写入主内存。所以,当 days 写入一个值的时候,变量 yearsmonths 的值都将写入主内存。

修改代码,加入读取函数:

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 值的时候,也会从主内存读取 monthsyears 的值。所以就能保证在上面读函数的中得到最新的 daysmonthsyears 的值。



指令重排序挑战

JVMCPU 可以通过重排序程序指令来提高性能,只要指令的语义仍旧保持不变。举个例子,看下面指令:

int a = 1;
int b = 2;

a++;
b++;

这些指令可以被重排序为下面序列,而且并没有失去程序语义:

int a = 1;
a++;

int b = 2;
b++;

然而,如果其中一个变量是 volatile,指令重排序将带来一个挑战。之前的示例代码如下:

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 一个值,那么新写入变量 yearsmonths 的值也将刷新回内存。但是,如果 JVM 重排序了指令如下:

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

monthsyears 的值仍旧会在变量 days 修改的时候写回主内存,但是这个动作发生在 monthsyears 修改之前。因此,新的值不能正确的显示给其它线程。这一次,重排序指令的语义改变了。

Java 针对这个问题提出一个解决方案,如下节所示。



Java volatile Happens-before 保证

为了解决指令重排序的挑战,Java volatile 关键字除了可见性保证外,还给予了一个 "happens-before" 保证。这个 happens-before 保证如下:

  1. 如果对其它变量的读写原先就发生在 volatile 变量的写操作之前,那么它们不会被重排序到 volatile 变量的写操作之后。在 volatile 变量的写操作之前的读写操作保证 happens-before 对这个 volatile 变量的读操作。注意,本身就在 volatile 变量写操作之后的读写操作仍旧有可能被重排序到之前执行。所以,在 volatile 变量读操作之后的读取操作重排序到之前执行是允许的,但是在 volatile 变量读操作之前的读取操作重排序到之后执行是不允许的。
  2. volatile 变量读操作之后的读取操作不允许重排序到之前发生。同样的,volatile 变量读操作之前的读取操作是可能被重排序到之后发生。

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



volatile 并不总是足够

即使 volatile 关键字保证了所有对 volatile 变量的读写操作都从主内存读取数据,仍旧存在仅声明变量为 volatile 不能保证安全性的场景。

之前的场景中,只有线程 1 写入共享变量 counter,所以声明 counter 变量为 volatile 能够确保线程 2 总是看见最新的值。

在多线程读取和写入 volatile 变量的短时间间隔内会创造一个竞争条件,即多线程可能会读取到同一个 volatile 变量值,生成了一个变量的新值,当新值写回到主内存时会被重载。

当多线程对同一个计数器进行递增操作,那么仅靠一个 volatile 不能满足这种场景。下面小节将详细讨论这种情况。

想象一下,线程 1 读取了共享变量 counter 的值 0 到缓存,进行递增操作;在线程 1 将数据写回主内存之前线程 2 也读取了 counter,同样进行递增操作。如下图所示:


这里写图片描述

线程 12 实际上没有进行同步。变量 counter 值应该是 2,但是每个线程在缓存中的结果为 1,而内存中的值为 0。即使线程将数据写回到主内存,结果仍旧是错误的。

个人见解

上面这种情况表明 volatile 无法保证非原子性操作不会受到其他线程的干扰(同步能保证),所以,遇到这种情况应该进行同步操作。



什么情况下 volatile 就足够了?

将向我之前提到的,如果两个线程对一个共享变量进行读写操作,那么仅使用 volatile 关键字不能保证安全性。在这种情况下,需要使用 synchronized 保证对变量的读写是原子性的。对 volatile 变量的读写不会阻塞线程,需要在临界区使用 synchronized 关键字。

除了使用 synchronized 以外,还可以使用 java.util.concurrent 包中的任何一种原子数据类型(atomic data type)。比如,AtomicLong 或者 AtomicReference 等等。

如果仅有一个线程对 volatile 变量执行读写操作,其它线程仅执行读操作,那么使用 volatile 即可保证每次线程读的变量数据都是最新值。如果没有 volatile 声明,无法保证安全性。

volatile 关键字保证可以工作在 32 位和 64 位变量(各种基本数据类型均可使用)。

个人见解

如果多线程应用,仅有一个线程进行写操作,其它进行读操作,那么只需要声明变量为 volatile 就能保证线程安全。

除了这种情况,其它情况下仅声明 volatile 不能保证线程安全,有两种方法:

  1. 使用 synchronized 关键字保证原子性;
  2. 使用原子数据类型


volatile 性能考虑

volatile 变量的读写将造成从主内存的读写操作,这比访问缓存更加耗费时间。对 volatile 变量的访问也阻止了指令重排序,影响了性能的优化。因此,只有强制需要变量的可见性情况下,才使用 volatile 声明

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

发布者:全栈程序员-用户IM,转载请注明出处:https://javaforall.cn/161291.html原文链接:https://javaforall.cn

【正版授权,激活自己账号】: Jetbrains全家桶Ide使用,1年售后保障,每天仅需1毛

【官方授权 正版激活】: 官方授权 正版激活 支持Jetbrains家族下所有IDE 使用个人JB账号...

(0)


相关推荐

  • ArcGIS10地理信息系统教程—从初学到精通—笔记(持续更新)

    ArcGIS10地理信息系统教程—从初学到精通—笔记(持续更新)arcgis10初学到精通—重要操作整理第二章ArcGIS快速入门1.设置相对路径37页2.选择要素48页3.超链接51页4.测量第三章地理数据库geodatabase有以下三种类型:文件地理数据库,个人地理数据库、arcsed数据库文件数据库:以文件夹的形式保存、管理。文件数据库可以由多个用户使用,但是同一数据在同一时间只能由一个用户编辑。个人…

  • java如何运行_如何运行java程序[通俗易懂]

    java如何运行_如何运行java程序[通俗易懂]我们在编写Java程序以后都会在集成开发环境中运行程序,那么该如何的在命令行中运行Java程序呢?下面动力节点java学院小编为大家介绍如何运行java程序?java程序的运行步骤1、首先我们在命令行运行Java程序需要借助jdk的环境依赖,打开jdk包,需要找到javac和java两个文件,如下图所示2、接下来我们需要打开运行窗口,然后在运行窗口中输入cmd命令,如下图所示3、在CMD命令行界面…

  • 去色怎么用_国际通用的最简单的救生包包括什么

    去色怎么用_国际通用的最简单的救生包包括什么2019独角兽企业重金招聘Python工程师标准>>>…

  • Web后端开发入门(2)

    Web后端开发入门(2)搭建JavaWeb应用开发环境–Tomcat服务器下载与安装首先,搜索Tomcat,找到如图网址点击,进入Tomcat官网在最左边一栏,有个Download,找到最新版Tomcat9,点击下拉,找到如上图所示位置,Core核心:zip版,tar.gz版(Linux系统),32位版,64位版,安装版。前几个版本都不需要安装,如果你需要安装就下载最后一个,然后选中自己要下载的版本,下载。安装…

  • java switch基础介绍及具体使用方法

    java switch基础介绍及具体使用方法switch的case语句可以处理int,short,byte,char类型的值,但是不能处理long,String等类型。javaswitch基础语法witch(表达式){case表达式常量1:语句1;break;case表达式常量2:语句2;break;……case表达式常量n:语句n;break;[default:语句n+1;]…

  • 本地tomcat 配置环境变量[通俗易懂]

    本地tomcat 配置环境变量[通俗易懂]1、官网下载tomcat,并解压Tomcat官网2、找到tomcat解压路径,配置三个环境变量新建CATALINA_HOME环境变量,CATALINA_HOME=E:\tomcat\apache-tomcat-8.5.38新建CATALINA_BASE环境变量,CATALINA_BASE=E:\tomcat\apache-tomcat-8.5.38…

发表回复

您的电子邮箱地址不会被公开。

关注全栈程序员社区公众号