Java Volatile关键字

Wesley13
• 阅读 593

点击上方"码之初"关注,···选择"设为星标"

与Java精品技术文章不期而遇

Java Volatile关键字

你若盛开,蝴蝶自来

面试系列结束了,昨天的面试终结篇竟然意外的受到了许多乡亲们的表扬,做这个公众号以来,第一次收到那么多在看,竟然还有打赏,码之初何德何能,虽然是无心插柳,却承蒙乡亲们如此厚爱,荣幸之至之余又感到受宠若惊,我开始真正意识到我可能是在做一件有意义的事。

你若盛开,蝴蝶自来。你若精彩,天自安排。所以,我只能不断学习,提高自己技能的同时,也努力为乡亲们带来更好的内容,和大家一起进步、成长,希望能和乡亲们互相陪伴,一起见证彼此从发展到发光。

学无止境,善始善终

学无止境,一段旅程结束了,短暂的休整之后我们就要重新踏上征程。 在做面试系列之前,码之初有两个未完的系列,一个是设计模式还有五六种没有更新完,一个是并发系列没有介绍完,做人要始终不渝,做事要善始善终,所以接下来的一段时间,码之初会将并发系列更新完,尽量在中间加一些有趣的内容,防止乡亲们觉得枯燥,弃我而去,哈哈。

Java volatile关键字用于将Java变量标记为“存储在主内存中”。更准确地说,这意味着每次对volatile变量的读取都将从计算机的主内存中读取,而不是从CPU缓存中读取,并且对volatile变量的每次写入都将被写入主内存中,而不仅是CPU缓存。

事实上,从Java 5开始,volatile关键字保证了volatile变量不仅仅是写入主内存或者是从主内存读取,我将从下面几个方面进行解释。

Java Volatile关键字

01、变量可见性问题

Java volatile关键字可确保跨线程更改变量的可见性。 这听起来有点抽象,下面来详细说明。

在多线程应用中,线程对非易失性变量进行操作,出于性能方面的考虑,每个线程在进行操作时都可以将主内存中的变量复制到CPU缓存中。 如果计算机包含多个CPU,则每个线程可能在不同的CPU上运行。 这意味着,每个线程可以将变量复制到不同CPU的CPU缓存中。 我们来看个例子:

Java Volatile关键字

对于非易失性变量,无法保证Java虚拟机(JVM)何时将数据从主存读取到CPU缓存,何时将数据从CPU缓存写入主存。 这可能会导致一些问题,这些都将在下面一起解释。

设想一种情况,有两个或多个线程可以访问一个共享对象,该共享对象包含一个声明为以下内容的计数器变量:

public class SharedObject {

想象一下,只有线程1递增计数器变量,但线程1和线程2都可能时不时地读取计数器变量。

如果计数器变量未声明为volatile,则无法保证计数器变量的值何时从CPU缓存写入主内存。 这意味着,CPU缓存中的计数器变量值可能与主内存中的不同。 这种情况如下所示:

Java Volatile关键字

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

Java Volatile关键字

02、Java volatile可见性保证

Java volatile关键字旨在解决变量可见性问题。 通过声明计数器变量为volatile,所有对计数器变量的写操作将立即写回到主内存中。 同样,计数器变量的所有读取将直接从主内存中读取。

我们对上面的计数器变量加上volatile关键字声明,像这样:

public class SharedObject {

因此,将变量声明为volatile可以保证对该变量的其他写入线程的可见性。

在上面给出的场景中,一个线程(T1)修改计数器,另一个线程(T2)读取计数器(但从不修改),声明计数器变量volatile足以保证T2对计数器变量的写入可见性。

但是,如果T1和T2都在递增计数器变量,那么声明计数器变量volatile是不够的,这个以后再说。

完全易失的可见性保****证(Full volatile Visibility Guarantee)

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

  • 如果线程A写入易失性变量,并且线程B随后读取相同的易失性变量,则在写入易失性变量之前线程A可见的所有变量在线程B读取易失性变量后也将可见。

  • 如果线程A读取了易失性变量,则在读取易失性变量时线程A可见的所有所有变量也将从主内存中重新读取。

我用代码示例来说明这一点:

public class MyClass {

udpate()方法写入三个变量,其中只有days是可变的。

完全易失的可见性保证意味着,当将值写入days时,线程可见的所有变量也将写入主内存。 这意味着,当将值写入days时,years和months的值也将写入主内存中。

在读取years,months和days的值时,可以这样:

public class MyClass {

注意:totalDays()方法首先将days的值读入total变量。 当读取days的值时,months和years的值也会被读入主内存,因此,可以保证按照上述读取顺序查看days,months和years的最新值。

Java Volatile关键字

03、指令重排挑战性

出于性能原因,允许Java VM和CPU对程序中的指令进行重新排序,只要指令的语义含义保持相同即可。 例如,看下面这个指令:

int a = 1;

这些指令可以重新排序为以下顺序,而不会丢失程序的语义:

int a = 1;

然而,当其中一个变量是volatile易失性变量时,指令重新排序是一个挑战。 让我们看一下前面示例中的MyClass类:

public class MyClass {

update()方法将值写入days后,新写入的years和months值也将写入主内存。 但是,如果Java VM重新对指令进行排列,比如:

public void update(int years, int months, int days){

当days变量被修改时,months和years的值仍会写入主内存,但这次是在新值写入months和years之前发生的。因此,新值对其他线程来说是不可见的。重新排序的指令的语义已更改。

Java有解决此问题的方法,我们将在后面看到。

Java Volatile关键字

04、Java易失性发生之前保证

为了解决指令重新排序的难题,除了可见性保证之外,Java volatile关键字还提供易失性发生之前(“happens-before”)保证。 发生之前保证保证了:

  • 如果读/写最初发生在对volatile变量的写入之前,则不能将对其他变量的读/写重新排序为在对volatile变量的写入之后发生。在写入volatile变量之前的读/写保证在写入volatile变量之前“发生”。注意,例如,在对volatile的写操作之后的其他变量的读/写操作仍有可能在对volatile的写操作之前重新排序。但不是相反。允许从后到前,但不允许从前到后。

  • 如果读取/写入最初发生在读取volatile变量之后,则不能将对其他变量的读取和写入重新排序为在读取volatile变量之前发生。注意,在volatile变量的读取之前发生的其他变量的读取可能会被重新排序为在volatile变量的读取之后发生。但不是相反。允许从前到后,但不允许从后到前。

上述“易失性发生之前(“happens-before”)确保强制执行volatile关键字的可见性保证。

05、声明volatile还不一定够

即使volatile关键字保证所有volatile变量的读取都直接从主存中读取,并且所有对volatile变量的写入都直接写入主存,但在某些情况下,声明变量volatile还不够。

在前面解释的只有线程1写入共享计数器变量的情况下,声明计数器变量为volatile,足以确保线程2始终看到最新的写入值。

实际上,如果写入变量的新值不依赖于先前的值,则多个线程甚至可能正在写入一个共享的volatile变量,并且仍将正确的值存储在主内存中。 换句话说,如果线程首先将值写入共享的volatile变量,则不需要先读取其值即可找出下一个值。

一旦线程需要首先读取volatile变量的值,并基于该值为共享的volatile变量生成新值,则volatile变量将不再足以保证正确的可见性。 读取volatile变量与写入新值之间的时间间隔很短,从而造成竞争状态,多个线程可能会读取volatile变量的同一个值,为该变量生成一个新值,并且在将该值写入主内存时 - 覆盖彼此的值。

多个线程递增同一个计数器的情况正是这样一种情况,即声明volatile变量还不够。 下面将更详细地解释此案例。

想象一下,如果线程1将一个值为0的共享计数器变量读入其CPU高速缓存中,将其递增为1,而不是将更改后的值写回到主内存中。 然后,线程2可以从主内存中(该变量的值仍为0)读取相同的计数器变量到其自己的CPU高速缓存中。 然后线程2还可将计数器增加到1,并且也不会将其写回主内存。 下图说明了这种情况:

Java Volatile关键字

线程1和线程2现在实际上不同步。 共享计数器变量的实际值应该是2,但每个线程的CPU缓存中都有该变量的值1,但在主内存中该值仍然为0。 这是糟糕的,即使线程最终将共享计数器变量的值写回主内存,该值也会出错。

06、什么时候声明volatile变量就足够

如前所述,如果两个线程都在读写一个共享变量,那么使用volatile关键字是不够的。 在这种情况下,您需要使用synchronized来保证变量的读写是原子的。 读取或写入volatile变量不会阻塞线程的读取或写入。 为此,必须在关键部分使用synchronized关键字。

作为同步块的替代方法,您还可以使用java.util.concurrent包中提供的许多原子数据类型之一。 例如,AtomicLong或AtomicReference或其他之一。

如果只有一个线程读取和写入volatile变量的值,而其他线程只读取该变量,那么读取线程将保证看到写入volatile变量的最新值。 如果不将变量声明volatile,就无法保证这一点。

volatile关键字保证可以在32位和64位变量上使用。

07、volatile的性能考虑

读写volatile变量会使该变量被读写到主存。 与访问CPU缓存相比,读写主内存的开销更大。 访问volatile变量还可以防止指令重新排序,这是一种常见的性能增强技术。 因此,只有在确实需要增强变量的可见性时,才应该使用volatile变量。

Java Volatile关键字

Java Volatile关键字

如果您觉得本文有参考价值,

麻烦您点“在看”鼓励一下,

或者点下文末在看支持一下,谢谢

往期精选

1、java.util.concurrent概述(译)

2、Java中Synchronized关键字简介(译)

Java Volatile关键字

本文分享自微信公众号 - 码之初(ma_zhichu)。
如有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。

点赞
收藏
评论区
推荐文章
blmius blmius
3年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
皕杰报表之UUID
​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为
待兔 待兔
4个月前
手写Java HashMap源码
HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程22
Jacquelyn38 Jacquelyn38
3年前
2020年前端实用代码段,为你的工作保驾护航
有空的时候,自己总结了几个代码段,在开发中也经常使用,谢谢。1、使用解构获取json数据let jsonData  id: 1,status: "OK",data: 'a', 'b';let  id, status, data: number   jsonData;console.log(id, status, number )
Stella981 Stella981
3年前
KVM调整cpu和内存
一.修改kvm虚拟机的配置1、virsheditcentos7找到“memory”和“vcpu”标签,将<namecentos7</name<uuid2220a6d1a36a4fbb8523e078b3dfe795</uuid
Wesley13 Wesley13
3年前
00:Java简单了解
浅谈Java之概述Java是SUN(StanfordUniversityNetwork),斯坦福大学网络公司)1995年推出的一门高级编程语言。Java是一种面向Internet的编程语言。随着Java技术在web方面的不断成熟,已经成为Web应用程序的首选开发语言。Java是简单易学,完全面向对象,安全可靠,与平台无关的编程语言。
Stella981 Stella981
3年前
Django中Admin中的一些参数配置
设置在列表中显示的字段,id为django模型默认的主键list_display('id','name','sex','profession','email','qq','phone','status','create_time')设置在列表可编辑字段list_editable
Stella981 Stella981
3年前
Docker 部署SpringBoot项目不香吗?
  公众号改版后文章乱序推荐,希望你可以点击上方“Java进阶架构师”,点击右上角,将我们设为★“星标”!这样才不会错过每日进阶架构文章呀。  !(http://dingyue.ws.126.net/2020/0920/b00fbfc7j00qgy5xy002kd200qo00hsg00it00cj.jpg)  2
Wesley13 Wesley13
3年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
Python进阶者 Python进阶者
10个月前
Excel中这日期老是出来00:00:00,怎么用Pandas把这个去除
大家好,我是皮皮。一、前言前几天在Python白银交流群【上海新年人】问了一个Pandas数据筛选的问题。问题如下:这日期老是出来00:00:00,怎么把这个去除。二、实现过程后来【论草莓如何成为冻干莓】给了一个思路和代码如下:pd.toexcel之前把这