Java volitile关键字html
Java volatile 关键字用来标记一个Java变量为“存储于主内存”。更准确地说是,每一次针对volatile变量的读操做将会从主内存读取而不是从CPU的缓存读取;每一次针对volatile变量的写操做都会写入主内存,而不只仅是写入CPU缓存。java
实际上,从Java 5开始,volatile关键字除了保证从主内存读写volatile变量之外,还保证了其余的一些东西。我将会在后面的部分进行解释。缓存
变量可见性问题多线程
Java volatile关键字保证变量值的变化在多个线程间的可见性。这个描述有些抽象,因此让我详细的解释一下。app
在一个多线程的程序里,若是线程操做一些非volatile的变量,为了提升性能,每个线程均可能会从主内存复制变量值到CPU缓存。若是你的电脑的CPU数量多于一个,不一样的线程可能会运行于不一样的CPU上。这意味着不一样的线程可能会把变量复制到不一样CPU的缓存中,如图所示:性能
对于使用非volatile的变量,Java虚拟机(JVM)将不会保证什么时候从主内存读取数据到CPU缓存,也不会保证什么时候把CPU缓存的数据写回到主内存。这将会形成一些问题。后面我将会详细解释。线程
设想如下情形,有两个或者两个以上的线程能够访问到一个包含了一个计数器的共享对象:3d
再设想一下,只有线程1增长counter变量,可是线程1和线程2会不时的读取counter变量。htm
若是counter变量没有被声明为volatile,counter变量的值将不会被保证什么时候才能从CPU缓存写回到主内存。这意味着counter变量在CPU缓存中的值可能和主内存中的值不同。这个情形如图所示:对象
因一个线程尚未把变量的值写回主内存,其余线程不能读取到这个变量最新的值的问题被称为“可见性”问题。一个线程的更改对于其余线程不可见。
Java volatile可见性保证
Java volatile关键字的目标就是解决变量的可见性问题。声明了带volatile的counter变量,全部对counter的写操做将会理解被写回到主内存。全部对counter变量的读操做也会从主内存读取。
如下是带了volatile的counter的声明:
声明一个变量为volatile由此能够保证其余线程对该变量的写操做的可见性。
在上面的情形中,一个线程(线程1)修改了counter,另外一个线程(线程2)读取了counter(可是从不会修改它),声明counter变量为volatile足以保证线程2对于针对counter变量写操做的可见性。
可是若是线程1和线程2都修改了counter的值,那么仅仅声明counter变量为volatile是不够的。后面会详细解释。
彻底的volatile可见性保证
实际上,Java volatile的可见性保证超出了volatile变量自己。可见性保证以下:
让咱们来看一个代码的例子:
update()方法写入三个变量,其中只有days是volatile的。
彻底的volatile可见性保证的意思是,当一个值被写入days的时候,全部对此线程可见的变量们将也会被写入主内存。也就是说,当一个值被写入days的时候,years和months的值也会被写入主内存。
当读取years,months和days的值的时候,你能够这样写:
注意totalDays()方法一上来就先读取days的值到total变量。当读取days的值,months和years也会从主内存读取。所以,使用上面的读取顺序,能够确保读取到days,months和years的最新的值。
指令重排序带来的挑战
因为性能方面的缘由,JVM和CPU只要可以保证指令的语义保持一致,是能够对指令进行从新排序的。好比下面的代码:
这些指令能够按照下面的顺序从新排序,可是并无丧失掉程序原来的语义:
可是当一些变量中的一个为volatile变量时,指令重排带来了挑战。让咱们看一下前面例子中的MyClass类。
当update()方法写入值到days的时候,years和months的新写入值也会写入主内存中。可是若是JVM像下面同样重排了这些指令的顺序怎么办:
当days变量更改时,months和years的值仍然会写入主内存,可是这时新的值尚未写入months和years。新的值所以没有适当的对其余线程可见。从新排序的指令的语义发生了改变。
Java针对此问题有一个解决方案。咱们将会在下一节看到。
Java volatile “以前发生(Happens-Before)”保证
为了应对指令重排序带来的挑战,除了可见性保证,Java volatile关键字还提供了“以前发生”(Happens-Before)保证。以前发生保证:
以上的“以前发生”保证确保了volatile关键字对于可见性的保证。
volatile并不老是足够的
虽然volatile关键字保证全部读取volatile变量都从主内存读取,而且全部写入volatile变量都直接写入主内存,可是仅仅声明变量为volatile仍然不够的情形依然存在。
在上面的情形中,只有线程1会写入共享的counter变量,声明counter为volatile能够足够保证线程2老是能看到最新的写入值。
实际上,若是新写入的变量值不依赖于变量的前值(换句话说就是,一个线程不须要经过先读取一个变量的值进而计算出新值),甚至多个线程能够写入一个共享的volatile变量,可是主内存中的变量值也是正确的。
当一个线程须要首先读取volatile变量的值,而后基于这个值生成这个共享的volatile变量的新值,仅仅声明变量为volatile就再也不可以保证变量的正确的可见性了。
从读取volatile变量到对此变量写入新值的这段很短的时间,会产生竞争情况。竞争情况在这里是指多个线程可能读取到volatile变量相同的值,为这个变量生成新值,当把值写回主内存时多个线程覆盖掉彼此的值。
多个线程同时增长同一个counter的值正是这样一个volatile变量不足以保证正确性的情形。后续将会详细解释这种情形。
假设线程1读取共享的counter变量值0到CPU缓存,增长这个值为1可是尚未把更改的值写回到主内存。线程2可能读取到此counter变量的值也是0,并放到它本身的CPU缓存。线程2接下来可能也增长counter的值为1,而且也不把更新的值写回到主内存。这个情形如图所示:
线程1和线程2实际上已经不一样步了。这个共享的counter变量的值本应该是2,可是每个线程在他们的CPU缓存中的值都是1,而主内存中的值还依然是0。这已经乱了。即便两个线程把值从CPU缓存写入主内存,值仍是错的。
何时volatile是足够的
正如我前面说的,若是两个线程会同时读取写入一个共享的变量,仅仅声明变量为volatile是不够的。这种情形你须要使用synchronized关键字来保证从读取到写入变量的原子性。读取或者写入一个volatile变量并不会阻塞其余线程的读写。若是想阻塞,你必须在临界区周围使用synchronized关键字。
做为synchronized关键字的替代,你也可使用java.util.concurrent包中的原子数据类型,好比AtomicLong或者AtomicReference等。
若是只有一个线程会读取和写入volatile变量,而其余的线程只会读取变量的值,那么读取值的线程将被保证能读到最新写入volatile变量的值。若是变量不声明为volatile,这将不能被保证。
volatile关键字支持32位和64位的变量。
volatile与性能
对volatile变量的读写会形成读写发生于主内存。对主内存读写的开销远远大于对CPU缓存的开销。对volatile变量的访问也会致使指令不能被重排序,而重排序是一种常规的提升性能的技术。所以你应该只在真正须要保证变量可见性的时候使用volatile变量。
译者总结:
只声明一个变量为volatile,而后读取的时候最早读取volatile变量,写入的时候最后写入volatile变量。
做者公众号(码年)扫码关注:
英文网址: