📜  Java中的volatile关键字

📅  最后修改于: 2022-05-13 01:55:11.361000             🧑  作者: Mango

Java中的volatile关键字

使用 volatile 是使类线程安全的另一种方式(如同步的原子包装器)。线程安全意味着一个方法或类实例可以被多个线程同时使用而没有任何问题。考虑下面的例子。

class SharedObj
{
   // Changes made to sharedVar in one thread
   // may not immediately reflect in other thread
   static int sharedVar = 6;
}

假设有两个线程在SharedObj上工作。如果两个线程在不同的处理器上运行,每个线程可能有自己的sharedVariable本地副本。如果一个线程修改了它的值,这种变化可能不会立即反映在主内存中的原始值中。这取决于缓存的写策略。现在另一个线程不知道导致数据不一致的修改值。

下图显示,如果两个线程运行在不同的处理器上,那么sharedVariable的值在不同的线程中可能是不同的。

挥发性Java

请注意,在没有任何同步操作的情况下写入普通变量可能对任何读取线程都不可见(这种行为称为顺序一致性)。尽管大多数现代硬件提供了良好的缓存一致性,因此,一个缓存中的更改很可能会反映在另一个缓存中,但依靠硬件“修复”有故障的应用程序并不是一个好习惯。

class SharedObj
{
   // volatile keyword here makes sure that
   // the changes made in one thread are 
   // immediately reflect in other thread
   static volatile int sharedVar = 6;
}

请注意,volatile 不应与 static 修饰符混淆。静态变量是所有对象共享的类成员。在主存储器中只有一个副本。

volatile vs synchronized:在我们继续之前,让我们看一下锁和同步的两个重要特性。

  1. 互斥:意味着一次只有一个线程或进程可以执行一段代码(临界区)。
  2. 可见性:这意味着一个线程对共享数据所做的更改对其他线程是可见的。

Java 的 synchronized 关键字保证了互斥和可见性。如果我们使修改共享变量值的线程块同步,则只有一个线程可以进入该块,并且它所做的更改将反映在主内存中。同时尝试进入该块的所有其他线程将被阻塞并进入睡眠状态。

在某些情况下,我们可能只需要可见性而不是原子性。在这种情况下使用同步是多余的,可能会导致可伸缩性问题。这里 volatile 来拯救。易失性变量具有同步的可见性特征,但没有原子性特征。 volatile 变量的值永远不会被缓存,所有的写入和读取都将在主内存中完成。但是,volatile 的使用仅限于非常有限的一组情况,因为大多数时候都需要原子性。例如,一个简单的增量语句,例如 x = x + 1;或 x++ 似乎是单个操作,但实际上是必须以原子方式执行的复合读-修改-写操作序列。

Java
// Java Program to demonstrate the
// use of Volatile Keyword in Java
 
public class VolatileTest {
    private static final Logger LOGGER
        = MyLoggerFactory.getSimplestLogger();
    private static volatile int MY_INT = 0;
   
    public static void main(String[] args)
    {
        new ChangeListener().start();
        new ChangeMaker().start();
    }
   
    static class ChangeListener extends Thread {
        @Override public void run()
        {
            int local_value = MY_INT;
            while (local_value < 5) {
                if (local_value != MY_INT) {
                    LOGGER.log(
                        Level.INFO,
                        "Got Change for MY_INT : {0}",
                        MY_INT);
                    local_value = MY_INT;
                }
            }
        }
    }
   
    static class ChangeMaker extends Thread {
        @Override public void run()
        {
            int local_value = MY_INT;
            while (MY_INT < 5) {
                LOGGER.log(Level.INFO,
                           "Incrementing MY_INT to {0}",
                           local_value + 1);
                MY_INT = ++local_value;
                try {
                    Thread.sleep(500);
                }
                catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}


输出(使用 Volatile 关键字):

Incrementing MY_INT to 1
Got Change for MY_INT : 1
Incrementing MY_INT to 2
Got Change for MY_INT : 2
Incrementing MY_INT to 3
Got Change for MY_INT : 3
Incrementing MY_INT to 4
Got Change for MY_INT : 4
Incrementing MY_INT to 5
Got Change for MY_INT : 5

输出(没有 volatile 关键字)

Incrementing MY_INT to 1
Incrementing MY_INT to 2
Incrementing MY_INT to 3
Incrementing MY_INT to 4
Incrementing MY_INT to 5

Java与 C/C++ 中的 volatile :

Java中的 Volatile 与 C/C++ 中的“volatile”限定符不同。对于Java,“volatile”告诉编译器一个变量的值永远不能被缓存,因为它的值可能会在程序本身的范围之外发生变化。在 C/C++ 中,开发嵌入式系统或设备驱动程序时需要“易失性”,您需要在其中读取或写入内存映射的硬件设备。特定设备寄存器的内容可能随时更改,因此您需要“volatile”关键字来确保编译器不会优化此类访问。