Java Volatile关键字

时间:2020-01-09 10:35:09  来源:igfitidea点击:

在这篇文章中,我们将看到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比使用锁便宜。