以下内容转自http://tutorials.jenkov.com/java-concurrency/volatile.html(使用谷歌翻译):

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

实际上,由于Java 5的volatile关键字保证不仅仅是volatile变量被写入和从主内存读取。我将在以下各节中解释一下。

Java volatile可见性保证

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

出于性能原因,线程在非volatile变量上运行的多线程应用程序中,每个线程可能会将变量从主存储器复制到CPU高速缓存中。如果你的计算机包含多个CPU,则每个线程可能在不同的CPU上运行。这意味着每个线程都可以将变量复制到不同CPU的CPU缓存中。这在这里说明了:

对于非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变量的所有读取将直接从主存储器读取。下面是如何volatile在声明counter 变量的样子:

public class SharedObject {

    public volatile int counter = 0;

}

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

Java volatile事件保证

由于Java 5的volatile关键字不仅仅保证了对变量的主内存的读取和写入。实际上,volatile关键字保证:

  • 如果线程A写入volatile变量和线程B随后读取相同的volatile变量,然后看到线程A的所有变量之前写volatile变量,也将是可见的线程B后,它已经读volatile变量。

  • volatile变量的读写指令不能被JVM重新排序(只要JVM从重新排序中没有检测到程序行为的变化,JVM可能会因为性能原因重新排序指令)。之前和之后的指令可以重新排序,但是这些指令不能混合写入或写入。无论读取还是写入volatile变量,任何指令都将保证在读取或写入后发生。

这些陈述需要更深入的解释。

当一个线程写入一个volatile变量时,不仅将volatile变量本身写入主存储器。在写入volatile变量之前,线程更改的所有其他变量也被刷新到主存储器。当一个线程读取一个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之前写入非volatile变量sharedObject.nonVolatile,所以当线程A写入sharedObject.counter(volatile变量)时,sharedObject.nonVolatile和sharedObject.counter都将写入主内存。

由于线程B从读取volatile的sharedObject.counter开始,所以sharedObject.counter和sharedObject.nonVolatile都从主内存读取到线程B使用的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()来不时地获取对象。只要线程A调用put()并且只有线程B调用take(),这个Exchanger可以使用volatile变量(不使用同步块)来正常工作。

但是,如果JVM可以在不改变重新排序的指令的语义的情况下,JVM可以重新排序Java指令来优化性能。如果JVM切换的读取和顺序写入里面会发生什么,put()take()?如果put()真的执行如下:

while(hasNewObject) {
//wait - do not overwrite existing new object
}
hasNewObject = true; //volatile write
object = newObject;

注意,在实际设置新对象之前,对volatile变量hasNewObject的写入将被执行。对于JVM,这可能看起来完全有效。两个写入指令的值不依赖于彼此。

但是,重新排序指令执行会损害对象变量的可见性。首先,线程B可能会在线程A实际上为对象变量写入一个新值之前看到hasNewObject设置为true。第二,现在甚至不能保证写入对象的新值将被刷新回主内存(以下是线程A在某处写入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个指令。

类似地,只要在所有这些指令之前发生易失性写入指令,JVM可以重新排序最后3条指令。在易失性写入指令之前,最后3条指令都不能重新排序。

这基本上是Java保护之前发生的volatile的意思。

volatile并不总是足够

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

在前面所述的情况下,只有线程1写入共享counter变量,声明counter变量volatile就足以确保线程2总是看到最新的写入值。

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

一旦线程需要首先读取volatile变量的值,并且基于该值为共享volatile变量生成新值,则变量volatile不再足以保证正确的可见性。在读取volatile变量和写入新值之间的短时间间隙创建了一个竞争条件 ,其中多个线程可能读取相同的volatile变量值,为变量生成一个新值,并将该值写回到主内存-覆盖彼此的值。

多线程增加相同计数器的情况正是这种情况,其中volatile变量不够。以下部分将更详细地解释这一情况。

想象一下,如果线程1将counter值为0的共享变量读入其CPU缓存,将其递增到1,而不是将更改的值写入主存储器。线程2然后可以从counter变量的值仍然为0的主存储器读取相同的变量到自己的CPU缓存中。线程2也可以将计数器递增到1,也不会将其写回主存储器。这种情况如下图所示:

线程1和线程2现在实际上不同步。共享counter变量的实际值应为2,但每个线程的CPU缓存中的变量的值为1,主内存中的值仍为0。这是一个混乱!即使线程最终将共享counter变量的值写回到主内存中,该值也将是错误的。

什么时候使用呢?

如前所述,如果两个线程都是共享变量的读取和写入,则使用volatile关键字是不够的。 在这种情况下,你需要使用synchronized来保证变量的读写是原子的。读取或写入volatile变量不阻止线程读取或写入。为了实现这一点,你必须在关键部分周围使用synchronized关键字。

作为synchronized块的替代,你还可以使用java.util.concurrent包中发现的许多原子数据类型之一。例如,AtomicLong或 AtomicReference其他人之一。

如果只有一个线程读写volatile变量的值,并且其他线程只读取变量,则读取线程将被保证看到写入volatile变量的最新值。在不变量变动的情况下,这不能保证。

volatile关键字保证在32位和64变量上工作。

性能考虑波动

读写volatile变量会导致变量被读取或写入主存储器。读取和写入主内存比访问CPU缓存更昂贵。访问volatile变量还可以防止指令重新排序,这是正常的性能增强技术。因此,当你真正需要强制实现变量的可见性时,你应该只使用volatile变量。

13、Java并发性和多线程-Java Volatile关键字的更多相关文章

  1. 11、Java并发性和多线程-Java内存模型

    以下内容转自http://ifeve.com/java-memory-model-6/: Java内存模型规范了Java虚拟机与计算机内存是如何协同工作的.Java虚拟机是一个完整的计算机的一个模型, ...

  2. 21、Java并发性和多线程-Java中的锁

    以下内容转自http://ifeve.com/locks/: 锁像synchronized同步块一样,是一种线程同步机制,但比Java中的synchronized同步块更复杂.因为锁(以及其它更高级的 ...

  3. 14、Java并发性和多线程-Java ThreadLocal

    以下内容转自http://ifeve.com/java-theadlocal/: Java中的ThreadLocal类可以让你创建的变量只被同一个线程进行读和写操作.因此,尽管有两个线程同时执行一段相 ...

  4. 12、Java并发性和多线程-Java同步块

    以下内容转自http://ifeve.com/synchronized-blocks/: Java 同步块(synchronized block)用来标记方法或者代码块是同步的.Java同步块用来避免 ...

  5. 22、Java并发性和多线程-Java中的读/写锁

    以下内容转自http://ifeve.com/read-write-locks/: 相比Java中的锁(Locks in Java)里Lock实现,读写锁更复杂一些.假设你的程序中涉及到对一些共享资源 ...

  6. java 并发性和多线程 -- 读感 (一 线程的基本概念部分)

    1.目录略览      线程的基本概念:介绍线程的优点,代价,并发编程的模型.如何创建运行java 线程.      线程间通讯的机制:竞态条件与临界区,线程安全和共享资源与不可变性.java内存模型 ...

  7. Java 并发和多线程(一) Java并发性和多线程介绍[转]

    作者:Jakob Jenkov 译者:Simon-SZ  校对:方腾飞 http://tutorials.jenkov.com/java-concurrency/index.html 在过去单CPU时 ...

  8. Java并发性和多线程

    Java并发性和多线程介绍   java并发性和多线程介绍: 单个程序内运行多个线程,多任务并发运行 多线程优点: 高效运行,多组件并行.读->操作->写: 程序设计的简单性,遇到多问题, ...

  9. Java并发性和多线程介绍

    java并发性和多线程介绍: 单个程序内运行多个线程,多任务并发运行 多线程优点: 高效运行,多组件并行.读->操作->写: 程序设计的简单性,遇到多问题,多开线程就好: 快速响应,异步式 ...

随机推荐

  1. DHTML_____document对象的方法

    <html> <head> <meta charset="utf-8"> <title>document对象的方法</titl ...

  2. win7任务计划提示”该任务映像已损坏或已篡改“怎么处理

    https://jingyan.baidu.com/article/e75057f2038e2febc91a8915.html 在命令行窗口(cmd)执行命令:schtasks /query /v   ...

  3. 22 C#中的异常处理入门 try catch throw

    软件运行过程中,如果出现了软件正常运行不应该出现的情况,软件就出现了异常.这时候我们需要去处理这些异常.或者让程序终止,避免出现更严重的错误.或者提示用户进行某些更改让程序可以继续运行下去. C#编程 ...

  4. 【hive】hive表很大的时候查询报错问题

    线上hive使用环境出现了一个奇怪的问题,跑一段时间就报如下错误: FAILED: SemanticException MetaException(message:Exception thrown w ...

  5. MVC之参数验证(三)

    在实际开发中,项目经理会一直强调一句话,永远不要相信客户端的数据(前端可以不用验证,但是后端必须验证).大家同意这样的说法吧..新端验证毋庸质疑JS验证,提高用户体验我们不得不添加一些与后端一致的验证 ...

  6. JavaScript(十四)经典的Ajax

    (function(){ //唯一向外暴露一个顶层变量 var myajax = window.myajax = {}; //作者.版本号信息 myajax.author = "maxwel ...

  7. NoSQL与关系数据库

    关系型数据库:完全支持关系代数理论作为基础:有较大的数据规模:固定的数据库模式:查询效率快:强一致性:数据完整性较易实现:扩展性一般:可用性好. NoSQL:部分支持关系代数理论作为基础:有超大数据规 ...

  8. Eclipse 编译java文件后出错 左树无红叉

    问题描述: 今天遇见让人郁闷的问题,在项目工程中java文件编译通不过,eclipse在java文件中标示错误,但是却不不能在navigator的视图中像平常一样出现小红叉.通过clean proje ...

  9. Caffe RPN:把RPN网络layer添加到caffe基础结构中

    在测试MIT Scene Parsing Benchmark (SceneParse150)使用FCN网络时候,遇到Caffe错误. 遇到错误:不可识别的网络层crop 网络层 CreatorRegi ...

  10. 第五届蓝桥杯校内选拔第六题_(dfs)

    你一定听说过“数独”游戏.如[图1.png],玩家需要根据9×9盘面上的已知数字,推理出所有剩余空格的数字,并满足每一行.每一列.每一个同色九宫内的数字均含1-9,不重复. 数独的答案都是唯一的,所以 ...