[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;}

如果只有 线程 1counter 变量进行自增操作,但 线程 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 。其次,这里无法确定新值是何时写回到主内存中的。(有可能是下次 线程 Avolatile 变量进行写操作时)。

为了防止以上情况的出现, 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 包中的许多原子数据类型。比如,AtomicLongAtomicReference 或其他类型中的一种。

在只有一个线程对 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 关键字介绍相关推荐

  1. 【Java并发编程 】同步——volatile 关键字

    英 /ˈvɒlətaɪl/ 我了太噢(记不住单词怎么读) 一.volatile的介绍? volatile是一个轻量级的synchronized,一般作用与变量,在多处理器开发的过程中保证了内存的可见性 ...

  2. 视频教程-Java并发编程实战-Java

    Java并发编程实战 2018年以超过十倍的年业绩增长速度,从中高端IT技术在线教育行业中脱颖而出,成为在线教育领域一匹令人瞩目的黑马.咕泡学院以教学培养.职业规划为核心,旨在帮助学员提升技术技能,加 ...

  3. 并发编程系列之volatile关键字详解

    并发编程系列之volatile关键字详解 1.volatile是什么? 首先简单说一下,volatile是什么?volatile是Java中的一个关键字,也是一种同步机制.volatile为了保证变量 ...

  4. 【Java并发编程】Java多线程(四):FutureTask 源码分析

    前言:[Java并发编程]Java多线程(三):Runnable.Callable --创建任务的方式 在上一篇文章的末尾我们通过两个问题,引出了 FutureTask 及其设计思路,先来回顾一下: ...

  5. 进阶笔记——java并发编程三特性与volatile

    欢迎关注专栏:Java架构技术进阶.里面有大量batj面试题集锦,还有各种技术分享,如有好文章也欢迎投稿哦.微信公众号:慕容千语的架构笔记.欢迎关注一起进步. 前言 前面讲过使用synchronize ...

  6. Java 并发编程CAS、volatile、synchronized原理详解

    CAS(CompareAndSwap) 什么是CAS? 在Java中调用的是Unsafe的如下方法来CAS修改对象int属性的值(借助C来调用CPU底层指令实现的): /*** * @param o ...

  7. java闭锁_【Java并发编程三】闭锁

    1.什么是闭锁? 闭锁(latch)是一种Synchronizer(Synchronizer:是一个对象,它根据本身的状态调节线程的控制流.常见类型的Synchronizer包括信号量.关卡和闭锁). ...

  8. java并发编程(三十五)——公平与非公平锁实战

    前言 在 java并发编程(十六)--锁的七大分类及特点 一文中我们对锁有各个维度的分类,其中有一个维度是公平/非公平,本文我们来探讨下公平与非公平锁. 公平|非公平 首先,我们来看下什么是公平锁和非 ...

  9. java并发编程实践学习---java的类锁和对象锁

    最近在看Java Concurrent in Practice(java并发编程实践),发现自己对java的线程.锁等机制,理解很肤浅,学习的也不够全面.打算借着这本书,全面的学习下JDK的并发包和一 ...

  10. Java并发编程学习笔记——volatile与synchronized关键字原理及使用

    Java代码在编译后会变成Java字节码,字节码被类加载器加载到JVM里,JVM执行字节码,最终需要转化为汇编指令在CPU上执行,Java中所使用的并发机制依赖于JVM的实现和CPU的指令. 一.vo ...

最新文章

  1. Other Linker Flags参数 -ObjC、-all_load和-force_load
  2. 【django轻量级框架】在线视频教育系统设计与实现
  3. Linux系统设置定时任务 1
  4. STM32 时钟系统
  5. 剑指offer之打印链表的倒数第N个节点的值
  6. Linux格式化分区的命令
  7. [ExtJS 6]Grid分页工具栏无效问题解决
  8. python安装教程-PyCharm 安装教程(Windows)
  9. Markdown和Html相互转换在线工具(Bejson)
  10. 关于vc6++编译DDK驱动出现的问题fatal error C1083: Cannot open include file: 'specstrings.h': No such file or dir
  11. iOS系统玩ONS游戏的详细说明(越狱,非越狱)
  12. JSzip 前端处理下载打包文件夹
  13. 使用DNSLog进行盲打
  14. java设计课堂派的教师端
  15. 你的善良必须有点锋芒
  16. 一个女大学生骂她男朋友的话,厉害,没一个脏字
  17. 九龙证券|游戏板块或继续迎来业绩估值“戴维斯双击”
  18. Unity数据持久化-Json
  19. 用Excel建立一个学生成绩表,包括学号、姓名、高数、英语,计算机,总分
  20. NiFi 模板(Template)简介

热门文章

  1. Web.xml配置详解之context-param (加载spring的xml,然后初始化bean看的)
  2. 仿分词统计的MapReduce 程序。
  3. ubuntu 12.04 以固定 IP 地址连接网络并配置DNS
  4. javascript cookies 存、取、删除实例【转】
  5. 数据导出到excel文件给客户端下载的几种方法
  6. php 转发请求及参数,php – Symfony 2转发请求传递GET / POST参数
  7. aes算法实现c语言_消息摘要算法MD5图解及C语言实现
  8. c语言第二章网上作业答案,c语言第二章课后习题答案
  9. c语言函数返回数组_C语言如何用一维数组拷贝函数,拷贝二位数组(C Primer Plus 10-7)...
  10. 为什么其他计算机连接需要密码是什么东西,连接其他电脑需要密码怎么处理