[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 write
return 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 write
object = 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 variable
int 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 。
参考
javamex: The volatile keyword in Java
结束
[Java并发编程(三)] Java volatile 关键字介绍的更多相关文章
- Java并发编程之三:volatile关键字解析 转载
目录: <Java并发编程之三:volatile关键字解析 转载> <Synchronized之一:基本使用> volatile这个关键字可能很多朋友都听说过,或许也都用过 ...
- Java并发编程学习:volatile关键字解析
转载:https://www.cnblogs.com/dolphin0520/p/3920373.html 写的非常棒,好东西要分享一下 Java并发编程:volatile关键字解析 volatile ...
- Java并发编程(二):volatile关键字
volatile是Java虚拟机提供的轻量级的同步机制.volatile关键字有如下两个作用,一句话概括就是内存可见性和禁止重排序. 1)保证被volatile修饰的共享变量对所有线程总是可见的,也就 ...
- Java并发编程之验证volatile不能保证原子性
Java并发编程之验证volatile不能保证原子性 通过系列文章的学习,凯哥已经介绍了volatile的三大特性.1:保证可见性 2:不保证原子性 3:保证顺序.那么怎么来验证可见性呢?本文凯哥(凯 ...
- Java并发编程:Java的四种线程池的使用,以及自定义线程工厂
目录 引言 四种线程池 newCachedThreadPool:可缓存的线程池 newFixedThreadPool:定长线程池 newSingleThreadExecutor:单线程线程池 newS ...
- Java并发编程基础之volatile
首先简单介绍一下volatile的应用,volatile作为Java多线程中轻量级的同步措施,保证了多线程环境中“共享变量”的可见性.这里的可见性简单而言可以理解为当一个线程修改了一个共享变量的时候, ...
- Java并发编程三个性质:原子性、可见性、有序性
并发编程 并发程序要正确地执行,必须要保证其具备原子性.可见性以及有序性:只要有一个没有被保证,就有可能会导致程序运行不正确 线程不安全在编译.测试甚至上线使用时,并不一定能发现,因为受到当时的 ...
- 干货:Java并发编程系列之volatile(二)
接上一篇<Java并发编程系列之synchronized(一)>,这是第二篇,说的是关于并发编程的volatile元素. Java语言规范第三版中对volatile的定义如下:Java编程 ...
- Java并发编程知识点总结Volatile、Synchronized、Lock实现原理
Volatile关键字及其实现原理 在多线程并发编程中,Volatile可以理解为轻量级的Synchronized,用volatile关键字声明的变量,叫做共享变量,其保证了变量的“可见性”以及“有序 ...
- JAVA多线程学习- 三:volatile关键字
Java的volatile关键字在JDK源码中经常出现,但是对它的认识只是停留在共享变量上,今天来谈谈volatile关键字. volatile,从字面上说是易变的.不稳定的,事实上,也确实如此,这个 ...
随机推荐
- wiki Confluence 百科介绍
Confluence是一个专业的wiki程序. 它是一个知识管理的工具, 通过它可以实现团队成员之间的协作和知识共享. Confluence不是一个开源软件, 非商业用途可以免费使用. Conflue ...
- php curl函数采集网页出现gzip压缩、编码不同导致的乱码 图文解决方法
方法一: header("content-type:text/html;charset=utf-8");$url="http://115.47.116.10/rest/k ...
- STM32——C语言课堂原代码
指针 /* ============================================================================ Name : Hello.c Au ...
- python网络编程(七)
应用:模拟QQ聊天 客户端参考代码 #coding=utf-8 from socket import * # 创建socket tcpClientSocket = socket(AF_INET, SO ...
- yii2 创建模块modules
方案一:如果模块儿较少,不用专门给模块儿目录定义别名,酱紫做就ok啦. 1.在项目根目录下面创建一个 modules 目录. 2.进入 gii : http://localhost/basic/web ...
- Yii Cache 缓存的使用
我的缓存组件配置在config\main.php文件,配置如下: 'components' => [ 'cache' => [ 'class' => 'yii\caching\Fil ...
- Java 接口 Closeable
该接口位于java.io包下,声明如下:public interface Closeable extends AutoCloseable.关闭流并释放与该流关联的所有系统资源.如果已经关闭该流,则调用 ...
- Application-Level层级异常捕获并定位程序的异常位置
最近遇到一个需求,在设置503错误页面时,如果程序出错,服务器自动发邮件通知所有程序员,方便程序员及时解决问题. 想到好像global.cs文件里面好像有个erro事件,然后找到了Applicatio ...
- JDBC(14)—对DAO进行改进修改
结构: DAO2_7< T >(接口)->DAOTestImpl< T >(实现类)->CustomerDAO(继承的子类)->CustomerDAOTest ...
- [原创] 上海招聘高级测试工程师(性能测试/自动化测试/App测试),长期有效
[原创] 上海招聘高级测试工程师(性能测试/自动化测试/App测试方向),长期有效 高级测试工程师(性能/自动化方向) 1.负责性能测试计划,性能需求分析,性能测试方案和用例设计,搭建性能测试环境,执 ...