以下内容转自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. SimpleDataFormat详解

    [转]SimpleDateFormat使用详解 public class SimpleDateFormat extends DateFormat SimpleDateFormat 是一个以国别敏感的方 ...

  2. nginx connect failed (110- Connection timed out) 问题排查

    首先排查 ping 下 nginx 与代理服务是否ping 的通,带端口的,telnet 下端口号是否是通的,本次遇到问题为 telnet 发现有台服务器不通,原因是端口未开放

  3. Spring.Net学习笔记(1)-容器的使用

    一.下载地址: http://www.springframework.net/download.html 二.相关程序集 Spring.Net容器定义在程序集Spring.Core.dll中,它依赖于 ...

  4. Android 新闻app的顶部导航栏,怎么实现动态加载?

    TabLayout + viewpager 其中viewpager的适配器要继承FragmentPagerAdapter,要实现动态更新,最主要的是适配器的写法,要在数据发生变化之后清除Fragmen ...

  5. Tomcat无法clean,无法remove部署的项目

    错误: 对部署在Tomcat下的项目进行clean操作,总是提示Could not load the Tomcat server configuration,错误信息如图: 解决: 原来是将Serve ...

  6. 微信小程序组件解读和分析:五、text文本

    text文本组件说明: text 文本就是微信小程序中显示出来的文本. text文本组件的示例代码运行效果如下: 下面是WXML代码: [XML] 纯文本查看 复制代码 ? 1 2 3 4 <v ...

  7. UVM基础之---------uvm factory机制base

    从名字上面就知道,uvm_factory用来制造uvm_objects和component.在一个仿真过程中,只有一个factory的例化存在. 用户定义的object和component types ...

  8. tensorFlow资源

    1,[莫烦]Tensorflow tutorials (Eng Sub) 神经网络 http://www.bilibili.com/video/av10118932/index_35.html#pag ...

  9. Layui数据表单的编辑

    使用layui对单元格进行编辑并保存 先是要引入layui的JS和CSS 然后创建一个表格 而重要的是edit这个属性,只有使用了这个属性的一列数据表格才可以编辑,其余的都不可以进行编辑 然后使用la ...

  10. 并发编程学习笔记(9)----AQS的共享模式源码分析及CountDownLatch使用及原理

    1. AQS共享模式 前面已经说过了AQS的原理及独享模式的源码分析,今天就来学习共享模式下的AQS的几个接口的源码. 首先还是从顶级接口acquireShared()方法入手: public fin ...