[Java并发编程(三)] Java volatile 关键字介绍
[Java并发编程(三)] Java volatile 关键字介绍
摘要
Java volatile 关键字是用来标记 Java 变量,并表示变量 “存储于主内存中” 。更准确的说就是对于 volatile 变量的每次读操作都是从计算机的主内存中读取,而不是 CPU 缓存,每次写操作也是将 volatile 变量写入主内存中,不是 CPU 缓存。
事实上,因为 Java 5 的 volatile 关键字保证的不止是从主内存读写。这点稍后会进行解释。
正文
Java volatile 可见性的保证
Java volatile 关键字保证了变量在跨线程时的变更可见性。这可能听起来比较抽象,所以下面举个例子来说明。
在多线程应用中,在线程操作 non-volatile 变量时,每个线程都会将变量从主内存拷贝到 CPU 缓存中然后进行处理,这主要是性能因素所决定的。如果计算机有不止一个 CPU ,每个线程会在不同的 CPU 上运行。也就是说,每个线程都会将变量拷贝至不同的 CPU 缓存中。如下图:
non-volatile 变量并不能保证 Java 虚拟机(JVM)将数据从主内存读入 CPU 缓存的时间,也无法确认 CPU 缓存的数据何时会被写入到主内存。这样会引发一些问题。
设想如果有两个或多个线程会访问一个共享对象如下:
public class SharedObject {public int counter = 0;}
如果只有 线程 1 对 counter 变量进行自增操作,但 线程 1 和 线程 2 都会时刻读取 counter 变量。
如果 counter 变量不是声明的 volatile ,那么并不能保证在写 counter 变量时,会将 CPU 缓存写会到主内存。也就是说, counter 变量在 CPU 缓存中的值与主内存中的值不一样。这种情况如下图所示:
因为变量的值还没有被另一线程写入主内存,线程无法就看到变量最新值。这种问题被称为 “可见性” 问题。一个线程的更新对其他线程是不可见的。
通过为 counter 变量声明 volatile 关键字,所有对于 counter 变量的写操作都会立即被写回到主内存中。同样,所有 counter 变量的读操作也会直接从主内存中直接读取。为 counter 变量声明 volatile 关键字的方式如下:
public class SharedObject {public volatile int counter = 0;}
Java volatile Happens-Before 保证
Java 5 的 volatile 关键字并不只是保证从主内存中读写变量。事实上, volatile 关键字还保证:
如果 线程 A 写如一个 volatile 变量,线程 B 接着读取同一个 volatile 变量,那么在写 volatile 变量前所有对 线程 A 可见的变量也会在 线程 B 读取该 volatile 变量后对 线程 B 可见。
volatile 变量的读写指令不允许被 JVM 重排(JVM 会在不影响程序行为前提下,为了提升性能对指令进行重排)。指令前和指令后可以被重排,但是 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 前,先写 non-volatile 变量 sharedObject.nonVolatile 变量,那么当 线程 A 写 sharedObject.counter( volatile 变量)时,sharedObject.nonVolatile 与 sharedObject.counter 都会写入主内存。
由于 线程 B 先读取 volatile 变量 sharedObject.counter ,那么 sharedObject.counter 和 sharedObject.nonVolatile 都会从主内存读入 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) {//wait - do not overwrite existing new object}object = newObject;hasNewObject = true; //volatile write}public Object take(){while(!hasNewObject){ //volatile read//wait - don't take old object (or null)}Object obj = object;hasNewObject = false; //volatile writereturn obj;}}
线程 A 会时不时通过调用 put() 方法设置对象。线程 B 会时不时通过调用 take() 方法获取对象。 Exchanger
可以只使用 volatile 变量(不使用 synchronized 块)就能保证程序的正确性,只要 线程 A 只调用 put() 而 线程 B 只调用 take() 。
但是,如果 JVM 在不改变语意的情况下,可能会为了优化性能对 Java 指令进行重排。如果 JVM 改变了 put() 和 take() 里的读写顺序会发生什么?如果 put() 的执行顺序是下面这样会怎样?
while(hasNewObject) {//wait - do not overwrite existing new object}hasNewObject = true; //volatile writeobject = newObject;
注意到写 volatile 变量 hasNewObject 发生在新对象设置前。对 JVM 来说这是完全有效的。两个写指令的值并不相互依赖。
但是,更改指令执行的顺序会损坏 object 变量的可见性。首先,线程 B 会在 线程 A 为 object 变量设置新值之前就看见 hasNewObject 设置成 true 。其次,这里无法确定新值是何时写回到主内存中的。(有可能是下次 线程 A 对 volatile 变量进行写操作时)。
为了防止以上情况的出现, volatile 关键字有 “发生前保证(happens before guarantee)”。 happens before guarantee 保证 volatile 变量的读写不能被重排。指令前和指令后可以被重排,但是 volatile 读写指令不能与在它之前或之后的指令重排。
看以下例子:
sharedObject.nonVolatile1 = 123;sharedObject.nonVolatile2 = 456;sharedObject.nonVolatile3 = 789;sharedObject.volatile = true; //a volatile variableint someValue1 = sharedObject.nonVolatile4;int someValue2 = sharedObject.nonVolatile5;int someValue3 = sharedObject.nonVolatile6;
JVM 会对前 3 个指令进行重排,因为它们对于 volatile 写指令都是 happens before (它们都必须在 volatile 写指令前执行)。
同样,只要 volatile 写指令在后 3 条指令前发生( happens before ),JVM 也可能对后 3 条指令进行重排。
以上是 Java volatile “happens before” 保证的基本含义。
volatile 并不总是有效
尽管 volatile 关键字可以保证所有 volatile 变量的读都直接访问主内存,所有 volatile 写都直接写入主内存,还是会有 volatile 失效的场景。
在之前描述的场景中,线程 1 写入共享变量 counter ,将 counter 变量声明 volatile 就可以保证 线程 2 总是可以看到最新的写入值。
事实上,多线程也可以写入同一 volatile 共享变量,如果新写入变量并不依赖于前序值,它仍然可以保证正确的值可以存入主内存。换句话说,如果线程将值写入共享 volatile 变量时不需要先读取它的值来计算下一个值时,就能有此保证。
只要线程需要先读取 volatile 变量的值,然后基于该值计算 volatile 变量的新值,那么 volatile 变量就无法保证它可见性的正确。在读取 volatile 变量与写入新值之间短暂的时间间隔会造成 竞争条件(Race Condition) ,这会导致多线程会读取 volatile 变量相同的值,并生成新值,当将值写回到主内存时,有可能会将它们生成的新值相互覆盖。
当多线程对相同的 counter 进行自增操作就是 volatile 变量无法保证可见性的典型场景。下面对这个例子进行更详细的解释。
设想 线程 1 读取共享变量 counter 的值 0 到 CPU 缓存,自增 1 后并没有将更新的值写回到主内存中。 线程 2 将相同的 counter 值 0 从主内存读入它自己的缓存,同时也将 counter 值自增到 1 ,并写回到主内存。这个场景下图所示:
线程 1 和 线程 2 并不同步(out of sync)。这时共享变量 counter 的真实值应该是 2 ,但是每个线程在它们自己的 CPU 缓存中存放的值都是 1 ,但是在主内存中,该值仍然是 0 。这很混乱!如果线程最终将 counter 值写回到主内存,那么该值也是错误的。
volatile 何时有效?
正如之前提到的,如果两个线程同时对一个共享变量进行读写操作,那么使用 volatile 关键字并不有效。这时就需要使用 synchronized 关键字来保证读与写都是原子操作(atomic)。读写 volatile 变量不能阻塞线程的读写。为了能实现阻塞,必须使用 synchronized 关键字划定关键区(critical section)。
替代 synchronized 块的方法可以使用 java.util.concurrent
包中的许多原子数据类型。比如,AtomicLong
或 AtomicReference
或其他类型中的一种。
在只有一个线程对 volatile 变量进行读写,而其他线程都仅对变量进行读取操作,那么可以保证 volatile 变量的最新写入值对读线程都可见。
volatile 关键字对于 32bit 和 64bit 变量都是有效的。
volatile 的性能考虑
volatile 变量的读写要求变量都从主内存中进行读写。读写主内存的代价要比访问 CPU 缓存高,访问 volatile 变量同样可以防止指令的重排(指令重排是一种常用的提高性能的技术)。因此,只有到真的需要保证变量的可见性的时候,才应该使用 volatile 变量。
附言
关于原子访问的解释,Oracle 官方有如下解释:
在程序中,原子操作(atomic action)可以保证所有的事情一并发生。原子操作不能在过程中停止:它要么完全发生,或要么完全不发生。在一个操作完成前,改原子操作产生的影响是不可见的。
在 c++ 中,自增表达式并不是原子操作。每个简单的表达式都可以是由多个复杂操作定义的,可以被分成多个操作。但是,原子操作是可以被区分的:
- 引用变量和大多数原始类型变量(除了 long 和 double)的读写都是原子操作
- 所有声明了 volatile 的变量(包括 long 和 double)的读写都是原子操作
原子操作不能重叠,这样它们就可以不受线程的干扰。不过,这并不能消除所有同步原子操作的需求,因为内存还是可能出现一致性错误。使用 volatile 变量可以降低内存一致性错误的风险,因为任何写 volatile 变量都会与之后续对相同变量的读操作建立 happens-before 的关系。这也意味着 volatile 变量对其他线程总是可见的。不仅仅如此,当一个线程对 volatile 变量进行读操作时,它看到的并不只是 volatile 变量的最新更改,同时还包括该更改所引起的副作用。
使用简单原子变量进行访问要比通过 synchronized 访问要更高效,但也需要更小心来避免内存一致性错误。可以更加应用的规模和复杂程度来判断额外的代价是否值得。
java.util.concurrent
包中的类提供了很多原子方法,并不依赖于 synchronization 。
参考
jenkov: Java Volatile Keyword
javamex: The volatile keyword in Java
oracle: Atomic Access
结束
[Java并发编程(三)] Java volatile 关键字介绍相关推荐
- 【Java并发编程 】同步——volatile 关键字
英 /ˈvɒlətaɪl/ 我了太噢(记不住单词怎么读) 一.volatile的介绍? volatile是一个轻量级的synchronized,一般作用与变量,在多处理器开发的过程中保证了内存的可见性 ...
- 视频教程-Java并发编程实战-Java
Java并发编程实战 2018年以超过十倍的年业绩增长速度,从中高端IT技术在线教育行业中脱颖而出,成为在线教育领域一匹令人瞩目的黑马.咕泡学院以教学培养.职业规划为核心,旨在帮助学员提升技术技能,加 ...
- 并发编程系列之volatile关键字详解
并发编程系列之volatile关键字详解 1.volatile是什么? 首先简单说一下,volatile是什么?volatile是Java中的一个关键字,也是一种同步机制.volatile为了保证变量 ...
- 【Java并发编程】Java多线程(四):FutureTask 源码分析
前言:[Java并发编程]Java多线程(三):Runnable.Callable --创建任务的方式 在上一篇文章的末尾我们通过两个问题,引出了 FutureTask 及其设计思路,先来回顾一下: ...
- 进阶笔记——java并发编程三特性与volatile
欢迎关注专栏:Java架构技术进阶.里面有大量batj面试题集锦,还有各种技术分享,如有好文章也欢迎投稿哦.微信公众号:慕容千语的架构笔记.欢迎关注一起进步. 前言 前面讲过使用synchronize ...
- Java 并发编程CAS、volatile、synchronized原理详解
CAS(CompareAndSwap) 什么是CAS? 在Java中调用的是Unsafe的如下方法来CAS修改对象int属性的值(借助C来调用CPU底层指令实现的): /*** * @param o ...
- java闭锁_【Java并发编程三】闭锁
1.什么是闭锁? 闭锁(latch)是一种Synchronizer(Synchronizer:是一个对象,它根据本身的状态调节线程的控制流.常见类型的Synchronizer包括信号量.关卡和闭锁). ...
- java并发编程(三十五)——公平与非公平锁实战
前言 在 java并发编程(十六)--锁的七大分类及特点 一文中我们对锁有各个维度的分类,其中有一个维度是公平/非公平,本文我们来探讨下公平与非公平锁. 公平|非公平 首先,我们来看下什么是公平锁和非 ...
- java并发编程实践学习---java的类锁和对象锁
最近在看Java Concurrent in Practice(java并发编程实践),发现自己对java的线程.锁等机制,理解很肤浅,学习的也不够全面.打算借着这本书,全面的学习下JDK的并发包和一 ...
- Java并发编程学习笔记——volatile与synchronized关键字原理及使用
Java代码在编译后会变成Java字节码,字节码被类加载器加载到JVM里,JVM执行字节码,最终需要转化为汇编指令在CPU上执行,Java中所使用的并发机制依赖于JVM的实现和CPU的指令. 一.vo ...
最新文章
- Other Linker Flags参数 -ObjC、-all_load和-force_load
- 【django轻量级框架】在线视频教育系统设计与实现
- Linux系统设置定时任务 1
- STM32 时钟系统
- 剑指offer之打印链表的倒数第N个节点的值
- Linux格式化分区的命令
- [ExtJS 6]Grid分页工具栏无效问题解决
- python安装教程-PyCharm 安装教程(Windows)
- Markdown和Html相互转换在线工具(Bejson)
- 关于vc6++编译DDK驱动出现的问题fatal error C1083: Cannot open include file: 'specstrings.h': No such file or dir
- iOS系统玩ONS游戏的详细说明(越狱,非越狱)
- JSzip 前端处理下载打包文件夹
- 使用DNSLog进行盲打
- java设计课堂派的教师端
- 你的善良必须有点锋芒
- 一个女大学生骂她男朋友的话,厉害,没一个脏字
- 九龙证券|游戏板块或继续迎来业绩估值“戴维斯双击”
- Unity数据持久化-Json
- 用Excel建立一个学生成绩表,包括学号、姓名、高数、英语,计算机,总分
- NiFi 模板(Template)简介
热门文章
- Web.xml配置详解之context-param (加载spring的xml,然后初始化bean看的)
- 仿分词统计的MapReduce 程序。
- ubuntu 12.04 以固定 IP 地址连接网络并配置DNS
- javascript cookies 存、取、删除实例【转】
- 数据导出到excel文件给客户端下载的几种方法
- php 转发请求及参数,php – Symfony 2转发请求传递GET / POST参数
- aes算法实现c语言_消息摘要算法MD5图解及C语言实现
- c语言第二章网上作业答案,c语言第二章课后习题答案
- c语言函数返回数组_C语言如何用一维数组拷贝函数,拷贝二位数组(C Primer Plus 10-7)...
- 为什么其他计算机连接需要密码是什么东西,连接其他电脑需要密码怎么处理