volatile关键字
谈谈你对 volatile 的理解?
你知道 volatile 底层的实现机制吗?
volatile 变量和 atomic 变量有什么不同?
volatile 的使用场景,你能举两个例子吗?
文章收录在 GitHub JavaKeeper ,包含 N 线互联网开发必备技能兵器谱
之前算是比较详细的介绍了 Java 内存模型——JMM, JMM是围绕着并发过程中如何处理可见性、原子性和有序性这 3 个 特征建立起来的,而 volatile 可以保证其中的两个特性,下面具体探讨下这个面试必问的关键字。
1. 概念
volatile 是 Java 中的关键字,是一个变量修饰符,用来修饰会被不同线程访问和修改的变量。
2. Java 内存模型 3 个特性
2.1 可见性
可见性是一种复杂的属性,因为可见性中的错误总是会违背我们的直觉。通常,我们无法确保执行读操作的线程能适时地看到其他线程写入的值,有时甚至是根本不可能的事情。为了确保多个线程之间对内存写入操作的可见性,必须使用同步机制。
可见性,是指线程之间的可见性,一个线程修改的状态对另一个线程是可见的。也就是一个线程修改的结果。另一个线程马上就能看到。
在 Java 中 volatile、synchronized 和 final 都可以实现可见性。
2.2 原子性
原子性指的是某个线程正在执行某个操作时,中间不可以被加塞或分割,要么整体成功,要么整体失败。比如 a=0;(a非long和double类型) 这个操作是不可分割的,那么我们说这个操作是原子操作。再比如:a++; 这个操作实际是a = a + 1;是可分割的,所以他不是一个原子操作。非原子操作都会存在线程安全问题,需要我们使用同步技术(sychronized)来让它变成一个原子操作。一个操作是原子操作,那么我们称它具有原子性。Java的 concurrent 包下提供了一些原子类,AtomicInteger、AtomicLong、AtomicReference等。
在 Java 中 synchronized 和在 lock、unlock 中操作保证原子性。
2.3 有序性
Java 语言提供了 volatile 和 synchronized 两个关键字来保证线程之间操作的有序性,volatile 是因为其本身包含“禁止指令重排序”的语义,synchronized 是由“一个变量在同一个时刻只允许一条线程对其进行 lock 操作”这条规则获得的,此规则决定了持有同一个对象锁的两个同步块只能串行执行。
3. volatile 是 Java 虚拟机提供的轻量级的同步机制
- 保证可见性
- 不保证原子性
- 禁止指令重排(保证有序性)
3.1 空说无凭,代码验证
3.1.1 可见性验证
class MyData { int number = 0; public void add() { this.number = number + 1; } } // 启动两个线程,一个work线程,一个main线程,work线程修改number值后,查看main线程的number private static void testVolatile() { MyData myData = new MyData(); new Thread(() -> { System.out.println(Thread.currentThread().getName()+"\t come in"); try { TimeUnit.SECONDS.sleep(2); myData.add(); System.out.println(Thread.currentThread().getName()+"\t update number value :"+myData.number); } catch (InterruptedException e) { e.printStackTrace(); } }, "workThread").start(); //第2个线程,main线程 while (myData.number == 0){ //main线程还在找0 } System.out.println(Thread.currentThread().getName()+"\t mission is over"); System.out.println(Thread.currentThread().getName()+"\t mission is over,main get number is:"+myData.number); } }
运行 testVolatile()
方法,输出如下,会发现在 main 线程死循环,说明 main 线程的值一直是 0
workThread execute workThread update number value :1
修改 volatile int number = 0
,,在 number 前加关键字 volatile,重新运行,main 线程获取结果为 1
workThread execute workThread update number value :1 main execute over,main get number is:1
3.1.2 不保证原子性验证
class MyData { volatile int number = 0; public void add() { this.number = number + 1; } } private static void testAtomic() throws InterruptedException { MyData myData = new MyData(); for (int i = 0; i < 10; i++) { new Thread(() ->{ for (int j = 0; j < 1000; j++) { myData.addPlusPlus(); } },"addPlusThread:"+ i).start(); } //等待上边20个线程结束后(预计5秒肯定结束了),在main线程中获取最后的number TimeUnit.SECONDS.sleep(5); while (Thread.activeCount() > 2){ Thread.yield(); } System.out.println("final value:"+myData.number); }
运行 testAtomic
发现最后的输出值,并不一定是期望的值 10000,往往是比 10000 小的数值。
final value:9856
为什么会这样呢,因为 i++
在转化为字节码指令的时候是4条指令
-
getfield
获取原始值 -
iconst_1
将值入栈 -
iadd
进行加 1 操作 -
putfield
把iadd
后的操作写回主内存
这样在运行时候就会存在多线程竞争问题,可能会出现了丢失写值的情况。
如何解决原子性问题呢?
加 synchronized
或者直接使用 Automic
原子类。
3.1.3 禁止指令重排验证
计算机在执行程序时,为了提高性能,编译器和处理器常常会对指令做重排,一般分为以下 3 种
处理器在进行重排序时必须要考虑指令之间的数据依赖性,我们叫做 as-if-serial
语义
单线程环境里确保程序最终执行结果和代码顺序执行的结果一致;但是多线程环境中线程交替执行,由于编译器优化重排的存在,两个线程中使用的变量能否保证一致性是无法确定的,结果无法预测。
我们往往用下面的代码验证 volatile 禁止指令重排,如果多线程环境下,`最后的输出结果不一定是我们想象到的 2,这时就要把两个变量都设置为 volatile。
public class ReSortSeqDemo { int a = 0; boolean flag = false; public void mehtod1(){ a = 1; flag = true; } public void method2(){ if(flag){ a = a +1; System.out.println("reorder value: "+a); } } }
volatile
实现禁止指令重排优化,从而避免了多线程环境下程序出现乱序执行的现象。
还有一个我们最常见的多线程环境中 DCL(double-checked locking)
版本的单例模式中,就是使用了 volatile 禁止指令重排的特性。
public class Singleton { private static volatile Singleton instance; private Singleton(){} // DCL public static Singleton getInstance(){ if(instance ==null){ //第一次检查 synchronized (Singleton.class){ if(instance == null){ //第二次检查 instance = new Singleton(); } } } return instance; } }
因为有指令重排序的存在,双端检索机制也不一定是线程安全的。
why ?
Because: instance = new Singleton();
初始化对象的过程其实并不是一个原子的操作,它会分为三部分执行,
- 给 instance 分配内存
- 调用 instance 的构造函数来初始化对象
- 将 instance 对象指向分配的内存空间(执行完这步 instance 就为非 null 了)
步骤 2 和 3 不存在数据依赖关系,如果虚拟机存在指令重排序优化,则步骤 2和 3 的顺序是无法确定的。如果A线程率先进入同步代码块并先执行了 3 而没有执行 2,此时因为 instance 已经非 null。这时候线程 B 在第一次检查的时候,会发现 instance 已经是 非null 了,就将其返回使用,但是此时 instance 实际上还未初始化,自然就会出错。所以我们要限制实例对象的指令重排,用 volatile 修饰(JDK 5 之前使用了 volatile 的双检锁是有问题的)。
4. 原理
volatile 可以保证线程可见性且提供了一定的有序性,但是无法保证原子性。在 JVM 底层是基于内存屏障实现的。
- 当对非 volatile 变量进行读写的时候,每个线程先从内存拷贝变量到 CPU 缓存中。如果计算机有多个CPU,每个线程可能在不同的 CPU 上被处理,这意味着每个线程可以拷贝到不同的 CPU cache 中
-
而声明变量是 volatile 的,JVM 保证了每次读变量都从内存中读,跳过 CPU cache 这一步,所以就不会有可见性问题
- 对 volatile 变量进行写操作时,会在写操作后加一条 store 屏障指令,将工作内存中的共享变量刷新回主内存;
- 对 volatile 变量进行读操作时,会在写操作后加一条 load 屏障指令,从主内存中读取共享变量;
通过 hsdis 工具获取 JIT 编译器生成的汇编指令来看看对 volatile 进行写操作CPU会做什么事情,还是用上边的单例模式,可以看到
(PS:具体的汇编指令对我这个 Javaer 太南了,但是 JVM 字节码我们可以认识,putstatic
的含义是给一个静态变量设置值,那这里的 putstatic instance
,而且是第 17 行代码,更加确定是给 instance 赋值了。果然像各种资料里说的,找到了 lock add1
据说还得翻阅。这里可以看下这两篇 https://www.jianshu.com/p/6ab7c3db13c3 、 https://www.cnblogs.com/xrq730/p/7048693.html )
有 volatile 修饰的共享变量进行写操作时会多出第二行汇编代码,该句代码的意思是对原值加零,其中相加指令addl前有 lock 修饰。通过查IA-32架构软件开发者手册可知,lock前缀的指令在多核处理器下会引发两件事情:
- 将当前处理器缓存行的数据写回到系统内存
- 这个写回内存的操作会引起在其他CPU里缓存了该内存地址的数据无效
正是 lock 实现了 volatile 的「防止指令重排」「内存可见」的特性
5. 使用场景
您只能在有限的一些情形下使用 volatile 变量替代锁。要使 volatile 变量提供理想的线程安全,必须同时满足下面两个条件:
- 对变量的写操作不依赖于当前值
- 该变量没有包含在具有其他变量的不变式中
其实就是在需要保证原子性的场景,不要使用 volatile。
6. volatile 性能
volatile 的读性能消耗与普通变量几乎相同,但是写操作稍慢,因为它需要在本地代码中插入许多内存屏障指令来保证处理器不发生乱序执行。
引用《正确使用 volaitle 变量》一文中的话:
很难做出准确、全面的评价,例如 “X 总是比 Y 快”,尤其是对 JVM 内在的操作而言。(例如,某些情况下 JVM 也许能够完全删除锁机制,这使得我们难以抽象地比较 volatile
和 synchronized
的开销。)就是说,在目前大多数的处理器架构上,volatile 读操作开销非常低 —— 几乎和非 volatile 读操作一样。而 volatile 写操作的开销要比非 volatile 写操作多很多,因为要保证可见性需要实现内存界定(Memory Fence),即便如此,volatile 的总开销仍然要比锁获取低。
volatile 操作不会像锁一样造成阻塞,因此,在能够安全使用 volatile 的情况下,volatile 可以提供一些优于锁的可伸缩特性。如果读操作的次数要远远超过写操作,与锁相比,volatile 变量通常能够减少同步的性能开销。
参考
《深入理解Java虚拟机》
http://tutorials.jenkov.com/java-concurrency/java-memory-model.html
https://juejin.im/post/5dbfa0aa51882538ce1a4ebc
《正确使用 Volatile 变量》https://www.ibm.com/developerworks/cn/java/j-jtp06197.html
低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
- 上一篇
全方位解析云数据库Redis灾备简介!
云栖号快速入门:【点击查看更多云产品快速入门】不知道怎么入门?这里分分钟解决新手入门等基础问题,可快速完成产品配置操作! 数据是很多业务的核心元素,作为数据载体的数据库承担着举足轻重的责任。云数据库Redis版作为高性能的Key-Value数据库,在业务场景中往往承载着大量的重要数据。本文将全方位地为您解析云数据库Redis版的灾备机制。 云数据库Redis版容灾架构演进 程序在运行过程中总会遇到各种各样的问题,例如程序BUG、设备故障、机房断电等,理想的容灾机制能够在这些问题发生时,保证数据的一致性和业务可用性。云数据库Redis版为了保证业务的高可用,不断提升容灾能力,为不同的业务场景提供相应的高可用方案。 下图反映了云数据库Redis版容灾架构的演进历史。 图 1. Redis容灾架构演进 当前三种方案同时存在,您可以根据业务需求选择合适的方案。下文将对各方案进行详细介绍。 单可用区高可用机制 Redis全架构均支持单机房HA高可用架构。HA监控系统采用独立的平台化架构,提供跨可用区的高可用机制,使云数据库Redis版比自建Redis更稳定。 标准版-双副本 标准版-双副本实例采...
- 下一篇
ThinkAdmin v6.0 发布,基于 ThinkPHP 6.0 的后台开发框架
大道至简 · 原生框架 非常感谢大家一直以来对 ThinkAdmin 的支持,ThinkAdmin 从 v1 到 v6 经历了几次大的调整,但总体都是基于 ThinkPHP 最新版本为核心在开发,以微信领域及最简后台为目标在设计。 由于现有功能并不能满足所有项目的需求,ThinkAdmin 只做基础底层的开发,这里包括系统权限管理,系统存储配置,微信授权管理,以及常用功能集成等…… 因此 ThinkAdmin 也被定性为外包二开基线项目,目前已经有许多公司及个人在使用。 ThinkAdmin v6 是基于 v1-v5 版本的积累,结合 ThinkPHP 6.0 的思维重新调整,减少大量原非必需的组件,自建存储层、服务层及任务机制,增加了许多友好指令! ThinkAdmin v6 经历了数个项目实践与测试,不停的调整,目前系统模块及微信模块已经趋于稳定,现将系统模块及微信定为 v6 内核两大模块发布,其他商城模块及相关辅助模块后续更进…… ThinkAdmin v6 新增可視化后台任务,可以实现大数据操作,前端实时进度显示与交互! 再次感谢大家对 ThinkAdmin 的支持!!! Th...
相关文章
文章评论
共有0条评论来说两句吧...
文章二维码
点击排行
-
Docker使用Oracle官方镜像安装(12C,18C,19C)
- Springboot2将连接池hikari替换为druid,体验最强大的数据库连接池
- CentOS8编译安装MySQL8.0.19
- Docker快速安装Oracle11G,搭建oracle11g学习环境
- SpringBoot2配置默认Tomcat设置,开启更多高级功能
- MySQL8.0.19开启GTID主从同步CentOS8
- CentOS7,8上快速安装Gitea,搭建Git服务器
- Jdk安装(Linux,MacOS,Windows),包含三大操作系统的最全安装
- SpringBoot2编写第一个Controller,响应你的http请求并返回结果
推荐阅读
最新文章
- 2048小游戏-低调大师作品
- Windows10,CentOS7,CentOS8安装MongoDB4.0.16
- Mario游戏-低调大师作品
- CentOS7安装Docker,走上虚拟化容器引擎之路
- CentOS8安装MyCat,轻松搞定数据库的读写分离、垂直分库、水平分库
- SpringBoot2编写第一个Controller,响应你的http请求并返回结果
- CentOS7,CentOS8安装Elasticsearch6.8.6
- SpringBoot2初体验,简单认识spring boot2并且搭建基础工程
- CentOS6,CentOS7官方镜像安装Oracle11G
- Docker使用Oracle官方镜像安装(12C,18C,19C)