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)
blank

相关推荐

  • leetcode难度级别_直线上最多的点数

    leetcode难度级别_直线上最多的点数给定一个二维平面,平面上有 n 个点,求最多有多少个点在同一条直线上。示例 1:输入: [[1,1],[2,2],[3,3]]输出: 3解释:^|| o| o| o +————->0 1 2 3 4示例 2:输入: [[1,1],[3,2],[5,3],[4,1],[2,3],[1,4]]输出: 4解释:^|| o| o o| o| o o+—–

  • Iocomp .NET WinForms OPC Crack「建议收藏」

    Iocomp .NET WinForms OPC Crack「建议收藏」Iocomp.NETWinFormsOPC包Iocomp.NETWinFormsOPCPack是一款独立产品,可将OPC功能添加到任何.NET控件。Ω578867473它还包括连接到Iocomp.NETWinForm控件上的复杂属性的高级功能。所有许可证购买都包括1年支持和维护。支持32位和64位Window操作系统。内置自定义属性编辑器,便于设置。100%托管代码与所有.NET语言兼容。所有属性、方法和事件的完整代码示例Io

  • 课程实验 【八路抢答器】

    课程实验 【八路抢答器】基于外部中断课程实验【八路抢答器】#defineucharunsignedchar#defineuintunsignedintsbitLED_main=P3^6;sbitKey=P3^0;ucharcodetabie[]={0x3f,0x06,0x5b,0x4f,0x66,0x6d,0x7d,0x07,0x7f,0x6f};ucharmain_go=0;//主线voidInit_E…

    2022年10月20日
  • Java学习笔记目录索引 (持续更新中)

    Java学习笔记目录索引 (持续更新中)Java学习路线目录索引一、Java基础(省略)Lambda表达式及函数式接口二、Java数据库MySQL一概念、DDL、DML、DQL、事务、约束等数据库设计一多表关系、三大范式JDBC一基本使用、DAO组件、连接池、JDBCTemplate三、JavaWebHTML相关学习CSS—常用属性CSS—选择器及三大特性CSS—网页的布局方式C…

  • dubbo入门详解[通俗易懂]

    dubbo入门详解[通俗易懂]dubbo分布式系统简介发展演变RPCdubbo核心概念搭建dubbo分布式系统简介“分布式系统是若干独立计算机的集合,这些计算机对于用户来说就像单个相关系统”分布式系统(distributed system)是建立在网络之上的软件系统。随着互联网的发展,网站应用的规模不断扩大,常规的垂直应用架构已无法应对,分布式服务架构以及流动计算架构势在必行,亟需一个治理系统确保架构有条不紊的演进。发展演变单一应用架构当网站流量很小时,只需一个应用,将所有功能都部署在一起,以减少部署节点和成本。此时

  • squid反向代理

    squid反向代理

发表回复

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

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