Java Volatile关键字
在这篇文章中,我们将看到Java中什么是volatile关键字,何时使用volatile变量以及将变量声明为volatile的原因是什么。
什么是Java中的volatile关键字
为了更好地了解Java中的volatile关键字,我们将必须对Java内存模型中的变量发生的优化有所了解。假设我们在代码中声明了一个变量测试。我们会认为测试变量将仅存储在RAM中,并且所有线程都将从那里读取测试变量的值。但是,为了使处理更快,处理器会将变量的值保存在其缓存中。在这种情况下,仅当高速缓存与内存之间发生同步时,对值的任何更改才会写回到主内存。
这将在多个线程正在读取或者写入共享变量时引起问题。如果我们以在多个线程中使用的测试变量为例,则可能出现以下情况:一个线程对仍存储在高速缓存中的测试变量进行了更改,而另一个线程试图从主内存中读取测试变量的值。这将导致内存不一致错误,因为不同的线程将读取/写入不同的测试变量值。
在Java中将变量声明为volatile如何有帮助
将变量声明为易失性可确保始终从主存储器读取变量的值。因此,在Java中将字段声明为volatile可以提供可见性,从而确保在随后每次对该字段进行读取之前,都会对volatile字段进行写操作。
我们在上面看到的问题是由于volatile字段不会发生CPU缓存的值,因为可以保证线程1对volatile变量所做的更新始终对线程2可见。
Java Volatile示例代码
Java中volatile关键字最常见的用法之一是声明为volatile的布尔状态标志,该标志指示事件的完成,以便另一个线程可以启动。
首先让我们看看如果在这种情况下不使用volatile会发生什么。
public class VolatileDemo { private static boolean flag = false; public static void main(String[] args) { // Thread-1 new Thread(new Runnable(){ @Override public void run() { for (int i = 1; i <= 2000; i++){ System.out.println("value - " + i); } // changing status flag flag = true; System.out.println("status flag changed " + flag ); } }).start(); // Thread-2 new Thread(new Runnable(){ @Override public void run() { int i = 1; while (!flag){ i++; } System.out.println("Start other processing " + i); } }).start(); } }
输出:
.... .... value - 1997 value - 1998 value - 1999 value - 2000 status flag changed true
运行此代码后,我们会看到第一个线程显示i直到2000的值并更改状态标志,但是第二个线程不会打印消息"开始其他处理",并且程序不会终止。由于while循环中线程2中经常访问flag变量,因此编译器可以通过将flag的值放在寄存器中来进行优化,然后它将继续测试循环条件(while(!flag)),而无需读取其中的值。主存储器中的标志。
现在,如果我们更改布尔变量标志并将其标记为volatile,则将保证一个线程对共享变量所做的更改对其他线程可见。
private static volatile boolean flag = false;
输出:
.... .... value - 1997 value - 1998 value - 1999 value - 2000 status flag changed true Start other processing 68925258
Volatile同时还确保语句的重新排序不会发生
当线程读取volatile变量时,它不仅可以看到对volatile的最新更改,还可以看到导致更改的代码的副作用。这也称为Java 5中volatile关键字提供的扩展保证之前的发生。
例如,如果线程T1在更新volatile变量之前更改了其他变量,则线程T2也将获得那些在线程T1中更新volatile变量之前已更改的变量的更新变量。
这将我们带到可能在编译时发生的重新排序以优化代码的地步。只要不改变语义,就可以对代码语句进行重新排序。
举个例子
private int var1; private int var2; private volatile int var3; public void calcValues(int var1, int var2, int var3){ this.var1 = 1; this.var2 = 2; this.var3 = 3; }
由于var3是易失性的,因此,由于在扩展保证之前发生,因此var1和var2的更新值也将被写入主内存,并且对其他线程可见。
如果将这些语句重新排序以进行优化怎么办。
this.var3 = 3; this.var1 = 1; this.var2 = 2;
现在,变量var1和var2的值在易失性变量var3更新后更新。因此,这些变量var1和var2的更新值可能不适用于其他线程。
因此,如果在更新其他变量之后对易失性变量进行读或者写操作,则不允许重新排序。
Volatile确保可见性而非原子性
在只有一个线程正在写入变量而其他线程仅在读取的情况下(如在状态标志的情况下),volatile有助于正确查看变量值。但是,如果许多线程正在读取和写入共享变量的值,那么volatile是不够的。在这种情况下,由于竞争条件,线程可能仍会获得错误的值。
让我们用一个Java示例清除它,其中有一个SharedData类,其对象在线程之间共享。在SharedData类中,计数器变量被标记为volatile。创建了四个线程,它们使计数器递增,然后显示更新的值。由于存在竞争条件,线程可能仍会获得错误的值。请注意,我们也可以在几次运行中获得正确的值。
public class VolatileDemo implements Runnable { SharedData obj = new SharedData(); public static void main(String[] args) { VolatileDemo vd = new VolatileDemo(); new Thread(vd).start(); new Thread(vd).start(); new Thread(vd).start(); new Thread(vd).start(); } @Override public void run() { obj.incrementCounter(); System.out.println("Counter for Thread " + Thread.currentThread().getName() + " " + obj.getCounter()); } } class SharedData{ public volatile int counter = 0; public int getCounter() { return counter; } public void incrementCounter() { ++counter; } }
输出:
Counter for Thread Thread-0 1 Counter for Thread Thread-3 4 Counter for Thread Thread-2 3 Counter for Thread Thread-1 3
有关Java中的volatile的要点
Java中的volatile关键字只能与变量一起使用,而不能与方法和类一起使用。
标记为volatile的变量可确保不缓存该值,并且对volatile变量的更新始终在主内存中进行。
Volatile还确保不会以可变的方式提供对语句的重新排序,而易失性提供了在扩展保证之前,即在更新保证之前,易失性变量更新之前对其他变量的更改也会写入主内存,并且对其他线程可见。
Volatile确保仅可见性而不是原子性。
如果最终变量也声明为volatile,则是编译时错误。
使用volatile比使用锁便宜。