博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
【11】Java中的volatile关键字
阅读量:6693 次
发布时间:2019-06-25

本文共 4977 字,大约阅读时间需要 16 分钟。

  hot3.png

  • 原文地址:
  • 作者: Jakob Jenkov

Java中的volatile关键是用于标记一个“存放在主存(内存)中的”变量。 更准确的说,是每次读取volatile变量都会从计算机主存(内存)读取,而不是从CPU的cache中读取。 而且,每次对volatile变量的写操作也是会立即写回主存,而不是仅仅写CPU cache。

事实上,从Java 5开始,volatile关键字不仅仅是保障了在主存中读/写,还有了其他的一些保障。下面就逐个来介绍这些特性。

volatile的可见性保障

volatile可以保障在跨线程的情况下对于变量的可见性。 接下来就详细说说这一点。

在多线程应用中,如果线程操作的是一个非volatile的普通变量,那么,出于性能的考虑,每个线程会将变量从主存中拷贝一份到CPU cache中来使用。 而且如果,计算机中有多个CPU(现在的CPU基本都是多核心),那每个线程还可能还会在不同的CPU上运行。 这就会导致,每个线程会将变量拷贝到不同的CPU中的cache里。例如下图:

image

在JVM从主存读取数据到CPU cache,或者从CPU cache中写回主存这两个过程中,对于非volatile的普通变量就无法得到保障。

(读取后可能就不会再次同步,写回的时间点也不确定)

想象一下这样的一个场景,有多个线程访问一个共享对象,其中包含一个counter变量。比如:

public class SharedObject {    public int counter = 0;}

再想象一下,如果只有线程A对counter变量进行递增,而线程A和线程B可能间歇性读取counter的值。

如果counter变量不是定义成volatile的,就无法保障counter变量修改后能即时从CPU cache中写回主存。 这就会导致,counter在CPU cache中与主存中的值,是不一致的。如下图所示:

image

由于没有写回主存,这就会导致其他线程看不到这个变量最新的值。这种情况,就被称之为:可见性问题。 一个线程对数据的修改,对其他线程不可见。

而如果将counter声明成volatile的,那么对于counter的修改就会立即写回主存。同样的,对于counter的读取也会直接从主存中读取。比如下面这样:

public class SharedObject {    public volatile int counter = 0;}

声明volatile,就可以在变量修改时,保障对其他线程的可见性。

volatile的Happens-Before原则

实际上,对于volatile是保障了以下两点:

  • 如果线程A对一个volatile进行了修改,而线程B随后进行读取,那么,在volatile变量写入之前,线程A所能看到的变量,在线程B读取volatile变量之后同样都是可见的。
  • 对于volatile变量的读/写指令,JVM不能进行重排序优化。对于volatile的读写指令,JVM必须保障先后顺序。

接下来就深入的聊一聊这个原则。

当一个线程对一个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对普通变量sharedObject.nonVolatile写入123,而这个操作发生在volatile变量sharedObject.counter的写操作之前。所以,sharedObject.nonVolatilesharedObject.counter都会在线程A对sharedObject.counter写操作时被写回主存。

当线程B开始读取sharedObject.counter变量时,sharedObject.nonVolatile变量也会一起从主存中读取到CPU cache中。 这就是说,线程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对puttake方法进行重排序,会怎样呢? 如果put方法变成下面这样:

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

volatile变量hasNewObject放到了object之前。 这在JVM看来时完全可以接受的。因为objecthasNewObject没有直接的依赖关系。

所以,重排序就会破坏object变量的可见性。 首先,当线程A还没有更新object,线程B就可以看到hasNewObject设置成了true。 其次,这个时候,就没有什么能够保障object能写回主存了(可能需要等到线程A再次对volatile变量更新的时候)。

为了防止上述情况的发生,volatile关键字引入了一条“happens-before”保障。 happens-before保障了对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写指令之前的。

同样的,JVM可以对最后3条指令进行重排序。

这就是volatile的Happens-before原则的基本含义。

volatile并不总是够用

虽然,volatile关键字能够保障变量的读取操作都是直接从主存中读取的,并且写操作也会直接写回主存。 但是,仅仅把变量定义成volatile,并不能足够的保障变量的并发安全。

在上文中,说过只有线程A更新counter变量,那么,将counter定义成volatile就足以保障线程B能够看到最新值。

但是,实际中如果多个线程对共享volatile变量进行写操作时,如果新值不依赖旧值(值可以直接覆盖),那这样也行。但是如果这个新值是需要在旧值的基础上做一些更新(比如计数器的递增)那这就会出问题。

比如,一个线程先读取volatile变量,并在这个值的基础上产生一个新值,那就无法保障得到一个正确的可见性。因为,在读取值与写回新值这个间隙,多线程进行这个操作时,就是在这个间隙中产生一个竞态条件。多个线程可能会彼此覆盖对方的值。

比如,多个线程对一个volatile变量进行递增操作时,就会发生上述的问题。

想象一下,如果线程A读取了counter变量的值为0,并放到了CPU cache中,然后,对其递增了1 ,但是还没写回主存。 这个时候,线程B读取了同样的值。 然后,线程B也对其递增了1,并且还没有写回主存。如下图所示:

image

这种情况下,线程A和B实际上没有进行同步。counter变量的值,应该是2,但是每个线程都会在CPU cache中更新为1,而主存中还是0。 这就出问题了。即使,线程把counter写回主存,这个值也是错误的。

那什么时候volatile足够保障并发安全呢?

如果有两个线程同时读写共享变量,那volatile是不足以保障并发安全的。 这种情况下,需要使用synchronized来保障变量读写操作的原子性。 读/写volatile变量不会阻塞线程,所以,需要使用在临界区使用synchronized关键字。

除了使用synchronized同步块之外,还可以使用java.util.concurrent包下的原子数据类型(AtomicXxxx)。例如:AtomicLongAtomicReference等。

如果是只有一个线程写,其他线程只是读取变量,那这个时候使用volatile是可以保障变量的最新值的可见性的。这种情况下,变量是并发安全的。

volatile关键字是可以保障32位和64位变量的

JVM变量是使用了一个32bit的solt,来存放变量的。如果是64位变量(如:long,double),就需要占用两个solt,那么就需要两次操作来完成它们的读/写。在并发情况下,这些操作也可能出现问题。

volatile的性能问题

volatile变量的读/写操作,会引发主存的变量读/写。 而主存的读写要远远慢于CPU cache的。 另外,volatile还会阻止重排序,而重排序是一种常见的性能优化方法。 因此,要注意,只在有必要时(并且是正确的情况下,如:一写多读),才使用volatile

转载于:https://my.oschina.net/roccn/blog/1517660

你可能感兴趣的文章
我的友情链接
查看>>
iostat命令解析
查看>>
linux Containers——试用lxc
查看>>
linux运维实战练习-2016年1月19日-2月3日课程作业(练习)安排
查看>>
行为型模式:中介者模式
查看>>
政府信息化建设重点——服务、多元化
查看>>
学习像树一样活着!
查看>>
Linux操作系统分析(10) - 进程通信之管道与信号量
查看>>
UpdateData()
查看>>
0001 kali linux 学习之起航篇
查看>>
在UnitedStack公有云上DevStack快速部署Openstack
查看>>
使用Xcode和Instruments调试解决iOS内存泄露
查看>>
Volley使用方法
查看>>
我的友情链接
查看>>
ASA防火墙的应用
查看>>
linux中telnet 带外管理服务器的设置
查看>>
用户登录认证
查看>>
Web版RSS阅读器(一)——dom4j读取xml(opml)文件
查看>>
百度UEditor编辑器ueditor.setContent总是报错
查看>>
属性化字符串问题集
查看>>