Java并发编程之volatile关键字
java Volatile关键字用于将java变量标记为“存储在主内存中”
意味着对易失性变量的每次读取都将从计算机的主存中读取,而不是从CPU的缓存中读取。而且对易失性变量的每次写入都将写入主存,而不仅仅是写入CPU缓存中。
变量的可见性
Java volatile关键字保证了变量更改在线程之间的可见性。在多线程应用程序中,线程对非易失性变量进行操作,出于性能方面的考虑,每个线程在对其进行操作时都可以将变量从主内存复制到CPU缓存中。 如果您的计算机包含多个CPU,则每个线程可能在不同的CPU上运行。 这意味着,每个线程可以将变量复制到不同CPU的CPU缓存中。
对于非易失性变量,无法保证Java虚拟机(JVM)何时将数据从主存读入CPU缓存,或何时将数据从CPU缓存写入主存。这可能会导致几个问题。如果变量未声明为volatile,则无法保证何时将变量的值从CPU缓存写入主存。这意味着,CPU缓存中的变量值可能与主存中的不同。
线程看不到变量的最新值,因为它还没有被另一个线程写回主存,这种问题称为“可见性”问题。一个线程的更新对其他线程是不可见的。
Java volatile 保证变量的可见性
Java volatile关键字旨在解决变量可见性问题。 通过声明变量为volatile,所有对变量的写操作将立即写回到主存储器。 同样,变量的所有读取将直接从主存储器读取。
将变量声明为volatile可以保证其他写入该变量的线程的可见性。
一个线程修改变量,另一个线程读取变量但从不修改它,因此声明变量为volatile就足以保证第一个线程写入变量时的可见性。
如果两个线程都进行修改变量,只申明为volatile是不行的。
如果线程A写入了一个volatile变量,而线程B随后又读取了同一个volatile变量,那么线程A在写入该volatile变量之前可见的所有变量,在线程B读取了该volatile变量之后也将可见。
如果线程A读取一个volatile变量,那么线程A在读取该volatile变量时可见的所有变量也将从主存中重新读取。
如果一个对象有多个变量,当一个变量申明为volatile变量,所有对线程可见的变量也会写到主存中。当我们修改volatile变量时,其他的变量也将写到主存中。
public class MyTimer{
private int year;
private int month;
private volatile int day;
public void update(int years, int months, int days){
this.years = years;
this.months = months;
this.days = days;
}
}
指令重排序问题
出于性能原因,允许Java VM和CPU对程序中的指令重新排序,只要指令的语义意义保持不变。
int num1; 重排 int num1;
int num2; num1++;
num1++; int num2;
num2 ;++ num2++
但是,当其中一个变量是volatile变量时,指令重新排序就会带来挑战.
如之前的例子:
public class MyTimer{
private int year;
private int month;
private volatile int day;
public void update(int years, int months, int days){
this.years = years;
this.months = months;
this.days = days;
}
}
重排后:
public class MyTimer{
private int year;
private int month;
private volatile int day;
public void update(int years, int months, int days){
this.days = days;
this.years = years;
this.months = months;
}
}
修改days变量时,月和年的值仍然会写入主存,但这次是在新值写入月和年之前。因此,不能正确地使新值对其他线程可见。重新排序的指令的语义已经改变。
Java volatile发生之前
为了解决指令重新排序的问题,Java volatile关键字提供了一个“happens-before”保证,以及可见性保证。
如果对volatile变量的写操作最初发生在对volatile变量的写操作之前,则对其他变量的读和写操作不能在写volatile变量后重新排序。
确保在对volatile变量进行写之前进行的读/写操作在“对volatile变量进行写之前”发生。 请注意,例如 读/写位于对volatile的写入之后的其他变量,以重新排序发生在对该volatile的写入之前。 并非相反。 从后到前是允许的,但从前到后是不允许的。
如果读取/写入最初发生在读取volatile变量之后,则对其他变量的读取和写入不能重新排列为在读取volatile变量之前发生。 请注意,可能会在读取volatile变量之后重新排列读取volatile变量之前发生的其他变量。 并非相反。 允许从之前到之后,但不允许从之后到之前。
volatile不是万能的
在前面解释的只有线程1写入共享变量的情况下,将变量声明为volatile就足以确保线程2总是看到最新的写入值。
事实上,如果写入变量的新值不依赖于它以前的值,多个线程甚至可以写入共享volatile变量,并且仍然将正确的值存储在主内存中。换句话说,如果向共享volatile变量写入值的线程不需要首先读取它的值来计算下一个值。
一旦线程需要首先读取volatile变量的值,并基于该值为共享的volatile变量生成新值,则volatile变量将不再足以保证正确的可见性。 读取volatile变量与写入新值之间的时间间隔很短,这造成了竞争状态,其中多个线程可能会读取volatile变量的相同值,为该变量生成一个新值,以及在写入值时 返回主内存-覆盖彼此的值。
多个线程修改同一个变量的情况恰恰是一个volatile变量不够用的情况。
设想一下,如果线程1将一个值为0的共享计数器变量读入其CPU缓存,将其增量为1,并且不将更改后的值写回主存。然后,线程2可以从主存中读取相同的计数器变量(该变量的值仍然为0)到它自己的CPU缓存中。然后,线程2也可以将计数器增加到1,并且不将计数器写回主存。
线程1和线程2现在实际上是不同步的。共享计数器变量的实际值应该是2,但是每个线程的CPU缓存中该变量的值都是1,而在主存中该值仍然是0。即使线程最终将共享计数器变量的值写回主存,该值也将是错误的。
如前所述,如果两个线程同时读取和写入共享变量,那么使用volatile关键字是不够的。在这种情况下,需要使用synchronized来保证对变量的读写是原子性的。读或写volatile变量不会阻塞线程的读或写。要做到这一点,您必须在关键部分使用synchronized关键字。
作为同步块的替代方法,您还可以使用java.util.concurrent包中提供的许多原子数据类型之一。 例如,AtomicLong或AtomicReference或其他之一。
volatile的性能问题
读取和写入volatile变量会使该变量被读取或写入主存储器。 与访问CPU高速缓存相比,读写主存储器的开销更大。 访问volatile变量还可以防止指令重新排序,这是正常的性能增强技术。因此,只有在真正需要增强变量的可见性时才应该使用volatile变量。