java关键字Volatile用于将java变量标记为存储在主内存中,这就意味着每次读取Volatile修饰的变量时都是从计算机的主内存中读取,而不是从CPU的缓存中读取,并且每次对Volatile变量写的时候都将写入主内存,而不仅仅是CPU缓存
可见性问题
Java关键字Volatile保证可以跨线程查看变量的变化,下面详细来说一下这个问题
在线程操作非Volatile变量的多线程应用程序中,出于性能原因,每个线程可以在处理它们时将变量从主内存复制到CPU高速缓存中。如果您的计算机包含多个CPU,则每个线程可以在不同的CPU上运行。这意味着,每个线程可以将变量复制到不同CPU的CPU缓存中。这在这里说明:
对于非Volatile变量,java虚拟机从主内存读取到CPU缓存,或从CPU缓存读取到主内存,这可能导致一系列问题:
假设两个线程或者多个线程访问一个共享对象,共享对象包含一个counter变量,像这样:
public class SharedObject {
public int counter = 0;
}
假设只有线程1递增counter变量,但线程1和线程2都可能时不时读取counter变量。
如果counter变量没有声明为Volatile,则无法保证counter从CPU缓存读取到主内存中的具体时间,这就意味着CPU缓存中的变量值可能跟主内存中变量值不同,这种情况如下所示:
因为还没有被线程写入主线程,而导致另一个线程没有看到变量的最新值得问题,被称为线程的“可见性”问题
,线程的更新操作对其他线程不可见
Java Volatile可见性保证
java Volatile关键帧意在解决线程的可见性问题,通过对counter声明volatile,对象counter所有写的操作将立即写入主内存,同时对counter的读操作也是直接访问主内存
public class SharedObject {
public volatile int counter = 0;
}
因此声明一个Volatile,可以保证对其他线程的可见性
在上面给出的场景中,一个线程(T1)修改计数器,另一个线程(T2)读取计数器(但从不修改它),声明了volatile的counter足以保证T2对counter变量写入的可见性。
但是,如果T1和T2都在增加counter变量,那么 counter变量声明volatile就不够了。稍后会详细介绍。
volatile完全可见性保证
实际上,volatile的可见性保证超出了volatile变量本身,可见性保证如下:
1.如果线程A 对volatile变量进行写操作,那么线程B可以立刻读取相同的volatile变量,在对volatile变量写前,所有的变量对线程A都是可见的。在读取volatile变量后对线程B同样是可见的。
2.如果线程A读取volatile变量,则读取变量时线程A的所有可见volatile变量也将从主内存重新读取
用实例代码来说明:
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
volatile完全可见性意味着,当对days进行写入时,线程所有的可见变量都会写入主内存(不仅仅是volatile变量自己写入到主存中,其他被该线程修改的所有变量也会刷新到主存),这就意味着,当对days进行写入时,years和months也将写入主内存,
当对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的值开始到获取total,当读取days时,years和months也会读取到主内存中,因此可以保证读取到days、years和months的最新值
指令重排
只要指令的语义含义不变,jvm和CPU就可以出于性能的原因,重新排序指令的程序,看以下说明:
int a = 1;
int b = 2;
a++;
b++;
这些指令可以按一下方式重排,而不会丢失程序的语义含义:
int a = 1;
a++;
int b = 2;
b++;
然而,当程序中有一个volatile字段时,指令重新排序提出了挑战。让我们从MyClass类中看看一下java volatile教程
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也会被写入主内存中,但是如果jvm把指令重排序了
public void update(int years, int months, int days){
this.days = days;
this.months = months;
this.years = years;
}
当days值被修改时,months和years仍然被写入主内存,但是这一次days的值的改变在months和years写入之前,因此新值没法正确的对其他线程可见,指令重排的语义以前被改变
Java volatile Happens-Before 保证
happens-before 关系是程序语句之间的排序保证,这能确保任何内存的写,对其他语句都是可见的。
为了解决指令重排挑战,volatile除了可见性保证之外,Java 关键字还提供“Happens-Before Guarantee”规则。"Happens-Before"保证:
如果线程A写入一个volatile变量,随后线程B读取相同的变量。那么变量对线程A来说在写入变量前就是可见的,对于B来说读取完变量后,对该变量也是可见的。
volatile变量的读和写指令不能由JVM重新排序()。读写指令前后可以重排序,但是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在什么时候使用
正如我前面提到的,如果两个线程都在读取和写入共享变量,那么使用 volatile
关键字是不够的。 在这种情况下,您需要使用synchronized来保证变量的读取和写入是原子性。读取或写入volatile变量不会阻止线程读取或写入。为此,您必须在关键部分周围使用synchronized
关键字。
作为synchronized
块的替代方法,您还可以使用java.util.concurrent
包中找到的众多原子数据类型之一。例如,AtomicLong
或者 AtomicReference
其他更多。
如果只有一个线程读取和写入volatile变量的值,而其他线程只读取变量,那么读取线程将保证看到写入volatile变量的最新值。则可以使用volatile关键词
该volatile
关键字适用于32位和64位变量。
volatile的性能因素
volatile变量会导致变量读取和写入主内存。读取和写入主内存比访问CPU缓存更昂贵。访问volatile变量也会阻止指令重新排序,这是一种正常的性能增强技术。因此,在真正需要强制实施变量可见性时,应该只使用volatile变量。