Java Volatile Keyword
Java Volatile Keyword
Java volatile关键字用于将Java变量标记为“存储在主存储器中”。 更确切地说,这意味着,每次读取一个volatile变量都将从计算机的主内存中读取,而不是从CPU缓存中读取,并且每次写入volatile变量都将写入主内存,而不仅仅是CPU缓存。
实际上,从Java 5开始,volatile关键字不仅仅保证将volatile变量写入主内存并从主内存中读取。 我将在以下部分解释。
Variable visibility Problems
Java volatile关键字可确保跨线程变量的可见性。 这可能听起来有点抽象,所以让我详细说明。
在线程操作非volatile变量的多线程应用程序中,出于性能原因,每个线程可以在处理它们时将变量从主存储器复制到CPU高速缓存中。 如果您的计算机包含多个CPU,则每个线程可以在不同的CPU上运行。 这意味着,每个线程可以将变量复制到不同CPU的CPU缓存中。 这在这里说明:
对于非volatile变量,无法保证Java虚拟机(JVM)何时将数据从主内存读取到CPU缓存中,或将数据从CPU缓存写入主内存。 这可能会导致一些问题,我将在以下部分中解释。
想象一下两个或多个线程可以访问共享对象的情况,该共享对象包含一个声明如下的计数器变量:
public class SharedObject {
public int counter = 0;
}
假设如果只有线程1递增计数器变量,但线程1和线程2都可能不时读取计数器变量.
如果计数器变量未声明为volatile,则无法保证何时将计数器变量的值从CPU高速缓存写回主存储器。 这意味着CPU缓存中的计数器变量值可能与主存储器中的计数器变量值不同。 这种情况如下所示:
线程没有看到变量的最新值的原因,是因为它还没有被另一个线程写回主内存,这就被被称为“可见性”问题。 其他线程看不到一个线程对非volatile共享变量的更新。
The Java volatile Visibility Guarantee
Java volatile关键字旨在解决可变可见性问题。 通过声明计数器变量volatile,对计数器变量的所有写操作都将立即写回主存储器。 此外,计数器变量的所有读取都将直接从主存储器中读取。
以下是计数器变量的volatile声明:
public class SharedObject {
public volatile int couter = 0;
}
因此,声明变量volatile可以保证对该变量的其他写入线程的可见性。
在上面给出的场景中,一个线程(T1)修改计数器,另一个线程(T2)读取计数器(但从不修改它),声明计数器变量volatile足以保证对计数器变量的写入T2的可见性。
但是,如果T1和T2都在递增计数器变量,那么声明计数器变量volatile是不够的。 稍后会详细介绍。
Full volatile Visibility Guarantee
实际上,Java volatile的可见性保证超出了volatile变量本身。 能见度保证如下:
- 如果线程A写入一个volatile变量而线程B随后读取相同的volatile变量,那么在写入volatile变量之前,线程A可见的所有变量在读取volatile变量后也将对线程B可见。
- 如果线程A读取volatile变量,则读取volatile变量时线程A可见的所有变量也将从主存储器重新读取。
让我用代码示例说明:
public class MyClass {
private int years;
private int months;
private volatile int days;
public void update(int years, int months, int days) {
this.years = years;
this.months = months;
this.days = days;
}
}
update()方法写入三个变量,其中只有days是volatile。
The full volatile visibility guarantee意味着,当一个值写入天数时,线程可见的所有变量也会写入主存储器。 这意味着,当一个值写入天数时,年和月的值也会写入主存。
在读years,months和days的值时,您可以这样做:
public class MyClass {
private int years;
private int months;
private volatile int days;
public int totalDays() {
int total = this.days;
total += months * 30;
total += years * 365;
return total;
}
public void update(int years, int months, int days) {
this.years = years;
this.months = months;
this.days = days;
}
}
请注意,totalDays()方法首先将天数值读入总变量。 在读取天数值时,月份和年份的值也会被读入主存储器中。 因此,您可以使用上述读取顺序查看days,months和years的最新值。
Instruction Reordering Challenges
为了提高性能,一般允许JVM和CPU在保证程序语义不变的情况下对程序中指令进行重排序,比如:
int a = 1;
int b = 2;
a++;
b++;
这些指令可以按以下顺序重新排序,而不会丢失程序的语义含义:
int a = 1;
a++
int b = 1;
b++;
然而,当其中一个变量是volatile修饰时,对指令重新排序提出了挑战。 让我们看看这个Java volatile教程前面的例子中的MyClass类:
public class MyClass {
private int years;
private int months;
private volatile int days;
public void update(int years, int months, int days) {
this.years = years;
this.months = months;
this.days = days;
}
}
一旦update()方法将值写入days,新写入的years和months也会写入主内存。 但是,如果Java VM重新排序指令,如下所示:
public void update(int years, int months, int days) {
this.days = days;
this.months = months;
this.years = years;
}
修改days变量时,仍会将months和years的值写入主内存,但这次是在将新值写入months和years之前。 因此,新值不能正确地对其他线程可见。 重新排序的指令的语义含义已经改变。
Java有一个解决这个问题的方法,我们将在下一节中看到。
The Java volatile Happens-Before Guarantee
为了解决指令重排序的挑战,除了可见性保证之外,Java volatile关键字还提供“happens-before”保证。 "happens-before"保证:
- 对其他变量的读写指令不能重排序到对一个volatile变量的写指令之后;
- 对其他变量的读写指令不能重排序到对一个volatile变量的读指令之前;
上述"happens-before"确保正在强制执行volatile关键字的可见性保证。
volatile is Not Always Enough
即使volatile关键字保证直接从主存储器读取volatile变量,并且所有对volatile变量的写入都直接写入主存储器,仍然存在声明变量volatile不足保证线程安全的情况。
在前面解释的情况中,只有线程1写入共享计数器变量,声明计数器变量volatile足以确保线程2始终看到最新的写入值。
一旦线程需要首先读取volatile变量的值,并且基于该值为共享volatile变量生成新值,volatile变量就不再足以保证正确的可见性。 读取volatile变量和写入新值之间的短时间间隔会产生竞态,其中多个线程可能读取volatile变量的相同值,为变量生成新值,并在写入值时 回到主内存 - 覆盖彼此的值。
多个线程递增相同计数器的情况正是这样一种情况,即volatile变量不够保证线程安全性。 以下部分更详细地解释了这种情况:
想象一下,如果线程1将值为0的共享计数器变量读入其CPU高速缓存,则将其递增为1并且还未将更改的值写回主存储器。 然后,线程2可以从主存储器读取相同的计数器变量,其中变量的值仍为0,进入其自己的CPU高速缓存。 然后,线程2也可以将计数器递增到1,也还未将其写回主存储器。 这种情况如下图所示:
线程1和线程2现在几乎不同步。 共享计数器变量的实际值应为2,但每个线程的CPU缓存中的变量值为1,而主存中的值仍为0.这是一个混乱! 即使线程最终将共享计数器变量的值写回主存储器,该值也将是错误的。
When is volatile Enough
正如我前面提到的,如果两个线程都在读取和写入共享变量,那么使用volatile关键字是不够的。 在这种情况下,您需要使用synchronized来保证变量的读取和写入是原子的。 读取或写入volatile变量不会阻塞其他线程读取或写入。 为此,您必须在关键部分周围使用synchronized关键字。
作为synchronized块的替代方法,您还可以使用java.util.concurrent包中的众多原子数据类型之一。 例如,AtomicLong或AtomicReference或其他之一。
如果只有一个线程读取和写入volatile变量的值,而其他线程只读取变量,那么读取线程将保证看到写入volatile变量的最新值。 如果不使变量变为volatile,则无法保证。
volatile关键字保证可以在32位和64位变量上运行。
Performance Considerations of volatile
从主存储器直接读入或写入比访问CPU低效的多, 而且volatile变量一定程度上阻止了指令重排序这一正常的性能增强技术. 因此,除非你确实需要强制实施变量可见性时,不然应尽量少用volatile修饰变量
翻译自: http://tutorials.jenkov.com/java-concurrency/volatile.html
原作者: Jakob Jenkov
Orz Orz Orz