Java之 volatile 关键字原理详解
程序员文章站
2022-06-17 09:36:51
...
一、什么是 volatile ?
为了更好地了解Java中的volatile关键字,您将必须对Java内存模型中的变量发生的优化有所了解。假设您在代码中声明了一个名为 test 的变量。您会认为 test 变量将仅存储在RAM中,并且所有线程都将从那里读取测试变量的值。但是,为了使处理更快,处理器会将变量的值保存在其缓存中。在那种情况下,仅当高速缓存和内存之间发生同步时,对值的任何更改才会写回到主内存。
这将在多个线程正在读取或写入共享变量的地方引起问题。如果我们以在多个线程中使用的 test 变量为例,则可能出现以下情况:一个线程对仍存储在高速缓存中的 test 变量进行了更改,而另一个线程试图从主内存中读取测试变量的值 。这将导致内存和高速缓存数据不一致错误,因为不同的线程将读取/写入不同的测试变量值。
二、如何声明 volatile ?
将变量声明为 volatile 可确保始终从主存储器读取变量的值。 因此,在Java中将字段声明为volatile 可以提供线程可见性。从而确保每次对volatile字段进行都写操作,都会早于随后读取该字段的操作,即:(当前线程的)写操作对随后的(其它线程的)读操作都是可见的。
我们在上面看到的问题是因为 volatile 字段不会发生CPU缓存的值,因为可以保证线程1对volatile变量所做的更新始终对线程2可见。
三、 volatile 使用示例
Java中volatile关键字最常见的用法之一是声明为volatile的布尔状态标志,该标志指示事件的完成,以便另一个线程可以启动。
首先让我们看看如果在这种情况下不使用volatile会发生什么。
OUTPUT
运行此代码后,您会看到第一个线程显示i直到2000的值,然后并更改状态标志。但是第二个线程不会打印消息“Start other processing ”,并且程序不会终止。 由于在while循环中的线程2中经常访问flag变量,因此编译器可以通过将flag的值放在缓存寄存器中来进行优化,然后它将继续测试循环条件(while(!flag)),而无需向主存储器中读取其中的值。
现在,如果您更改布尔变量标志并将其标记为volatile,这将确保一个线程对共享变量所做的更改对其他线程可见。
OUTPUT:结果正确
四、 volatile 受变量声明的顺序的影响
当线程读取一个volatile变量时,它不仅会看到对volatile的最新更改,还会看到导致更改的代码的副作用。 这也称为(happens-before-extended-guarantee),由Java 5中的volatile 关键字提供。
例如,如果线程T1在更新volatile变量之前更改了其他变量,则线程T2也将获得那些在线程T1中更新volatile变量之前已更改的变量的更新变量。
这将我们带到可能在编译时发生的重新排序以优化代码的地步。 只要不改变语义,就可以对代码语句进行重新排序。
由于var3是 volatile 的,由于 happens-before extended guarantee,因此var1和var2的更新值也将被写入主内存,并且对其他线程可见。
如果将这些语句重新排序以进行优化怎么办。
现在,变量var1和var2的值在 volatile 变量var3更新后更新。 因此,这些变量var1和var2的更新值可能不可用于其他线程。
这就是为什么如果在更新其他变量之后对 volatile 变量进行读取或写入,则不允许重新排序的原因。
五、 volatile 只能保证 可见性,不能保证原子性
在只有一个线程正在写入变量而其他线程仅在读取的情况下(如在状态标志的情况下),volatile有助于正确查看变量值。 但是,如果许多线程正在读取和写入共享变量的值,那么volatile是不够的。 在那种情况下,由于竞争条件,线程可能仍会得到错误的值。
让我们用一个Java示例来说清楚,其中有一个SharedData类,其对象在线程之间共享。 在SharedData类中,计数器变量被标记为volatile。 创建了四个线程,它们使计数器递增,然后显示更新的值。 由于存在竞争条件,线程可能仍会获得错误的值。 请注意,您也可以在几次运行中获得正确的值。
OUTPUT
六、 volatile 要点总结
在Java中volatile关键字只能与不带的方法和类变量使用。
标记为volatile的变量可确保不缓存该值,并且对volatile变量的更新始终在主内存中进行。
volatile 也保证了报表的重新排序不会发生这样的挥发性提供的之前发生延长的保证下volatile变量的更新之前更改其他变量也被写入主存储器和可见的其他线程。
volatile 确保仅可见性而不是原子性。
如果 final 变量也声明为 volatile,则是编译时错误。
使用 volatile 比使用 Lock 便宜。
-
Refer: https://knpcode.com/java/multi-threading/volatile-keyword-in-java/
-
为了更好地了解Java中的volatile关键字,您将必须对Java内存模型中的变量发生的优化有所了解。假设您在代码中声明了一个名为 test 的变量。您会认为 test 变量将仅存储在RAM中,并且所有线程都将从那里读取测试变量的值。但是,为了使处理更快,处理器会将变量的值保存在其缓存中。在那种情况下,仅当高速缓存和内存之间发生同步时,对值的任何更改才会写回到主内存。
这将在多个线程正在读取或写入共享变量的地方引起问题。如果我们以在多个线程中使用的 test 变量为例,则可能出现以下情况:一个线程对仍存储在高速缓存中的 test 变量进行了更改,而另一个线程试图从主内存中读取测试变量的值 。这将导致内存和高速缓存数据不一致错误,因为不同的线程将读取/写入不同的测试变量值。
二、如何声明 volatile ?
将变量声明为 volatile 可确保始终从主存储器读取变量的值。 因此,在Java中将字段声明为volatile 可以提供线程可见性。从而确保每次对volatile字段进行都写操作,都会早于随后读取该字段的操作,即:(当前线程的)写操作对随后的(其它线程的)读操作都是可见的。
我们在上面看到的问题是因为 volatile 字段不会发生CPU缓存的值,因为可以保证线程1对volatile变量所做的更新始终对线程2可见。
三、 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(); } }
OUTPUT
.... .... value - 1997 value - 1998 value - 1999 value - 2000 status flag changed true
运行此代码后,您会看到第一个线程显示i直到2000的值,然后并更改状态标志。但是第二个线程不会打印消息“Start other processing ”,并且程序不会终止。 由于在while循环中的线程2中经常访问flag变量,因此编译器可以通过将flag的值放在缓存寄存器中来进行优化,然后它将继续测试循环条件(while(!flag)),而无需向主存储器中读取其中的值。
现在,如果您更改布尔变量标志并将其标记为volatile,这将确保一个线程对共享变量所做的更改对其他线程可见。
private static volatile boolean flag = false;
OUTPUT:结果正确
.... .... value - 1997 value - 1998 value - 1999 value - 2000 status flag changed true Start other processing 68925258
四、 volatile 受变量声明的顺序的影响
当线程读取一个volatile变量时,它不仅会看到对volatile的最新更改,还会看到导致更改的代码的副作用。 这也称为(happens-before-extended-guarantee),由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是 volatile 的,由于 happens-before extended guarantee,因此var1和var2的更新值也将被写入主内存,并且对其他线程可见。
如果将这些语句重新排序以进行优化怎么办。
this.var3 = 3; this.var1 = 1; this.var2 = 2;
现在,变量var1和var2的值在 volatile 变量var3更新后更新。 因此,这些变量var1和var2的更新值可能不可用于其他线程。
这就是为什么如果在更新其他变量之后对 volatile 变量进行读取或写入,则不允许重新排序的原因。
五、 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; } }
OUTPUT
Counter for Thread Thread-0 1 Counter for Thread Thread-3 4 Counter for Thread Thread-2 3 Counter for Thread Thread-1 3
六、 volatile 要点总结
在Java中volatile关键字只能与不带的方法和类变量使用。
标记为volatile的变量可确保不缓存该值,并且对volatile变量的更新始终在主内存中进行。
volatile 也保证了报表的重新排序不会发生这样的挥发性提供的之前发生延长的保证下volatile变量的更新之前更改其他变量也被写入主存储器和可见的其他线程。
volatile 确保仅可见性而不是原子性。
如果 final 变量也声明为 volatile,则是编译时错误。
使用 volatile 比使用 Lock 便宜。
-
Refer: https://knpcode.com/java/multi-threading/volatile-keyword-in-java/
-