Guide to the Volatile Keyword in Java – Java中的挥发性关键字指南

最后修改: 2017年 8月 20日

中文/混合/英文(键盘快捷键:t)

1. Overview

1.概述

In the absence of necessary synchronizations, the compiler, runtime, or processors may apply all sorts of optimizations. Even though these optimizations are beneficial most of the time, sometimes they can cause subtle issues.

在没有必要同步的情况下,编译器、运行时或处理器可能会应用各种优化。尽管这些优化在大多数时候是有益的,但有时也会引起一些微妙的问题。

Caching and reordering are among those optimizations that may surprise us in concurrent contexts. Java and the JVM provide many ways to control memory order, and the volatile keyword is one of them.

缓存和重新排序是那些在并发情况下可能让我们感到惊讶的优化。Java和JVM提供了许多方法来控制内存顺序,而volatile关键字就是其中之一。

In this tutorial, we’ll focus on the foundational, but often misunderstood, concept in the Java language, the volatile keyword. First, we’ll start with a bit of background about how the underlying computer architecture works, and then we’ll get familiar with memory order in Java.

在本教程中,我们将重点讨论Java语言中的基础性概念,即volatile关键字,但经常被误解。首先,我们先了解一下计算机底层架构的背景,然后再熟悉一下Java中的内存顺序。

2. Shared Multiprocessor Architecture

2.共享的多处理器结构

Processors are responsible for executing program instructions. Therefore, they need to retrieve both the program instructions and required data from RAM.

处理器负责执行程序指令。因此,它们需要从RAM中检索程序指令和所需数据。

As CPUs are capable of carrying out a significant number of instructions per second, fetching from RAM isn’t that ideal for them. To improve this situation, processors are using tricks like Out of Order Execution, Branch Prediction, Speculative Execution, and, of course, Caching.

由于CPU每秒能够执行相当数量的指令,从RAM中获取指令对它们来说并不那么理想。为了改善这种情况,处理器正在使用诸如顺序外执行分支预测累积执行等技巧,当然还有缓存。

This is where the following memory hierarchy comes into play:

这就是以下内存层次的作用所在。

cpu

As different cores execute more instructions and manipulate more data, they fill up their caches with more relevant data and instructions. This will improve the overall performance at the expense of introducing cache coherence challenges.

随着不同的内核执行更多的指令和操作更多的数据,它们会用更多的相关数据和指令填满它们的缓存。这将提高整体性能,但代价是引入高速缓存一致性挑战

Simply put, we should think twice about what happens when one thread updates a cached value.

简单地说,当一个线程更新一个缓存值时,我们应该三思而行。

3. When to Use volatile

3.何时使用volatile

In order to expand more on the cache coherence, we’ll borrow an example from the book Java Concurrency in Practice:

为了更多的扩展缓存一致性,我们将从Java Concurrency in Practice一书中借用一个例子。

public class TaskRunner {

    private static int number;
    private static boolean ready;

    private static class Reader extends Thread {

        @Override
        public void run() {
            while (!ready) {
                Thread.yield();
            }

            System.out.println(number);
        }
    }

    public static void main(String[] args) {
        new Reader().start();
        number = 42;
        ready = true;
    }
}

The TaskRunner class maintains two simple variables. In its main method, it creates another thread that spins on the ready variable as long as it’s false. When the variable becomes true, the thread will simply print the number variable.

TaskRunner 类维护两个简单的变量。在其主方法中,它创建了另一个线程,只要ready变量为false,它就会在该变量上旋转。当该变量变为真时,线程将简单地打印数字变量。

Many may expect this program to simply print 42 after a short delay; however, in reality, the delay may be much longer. It may even hang forever, or print zero.

许多人可能期望这个程序在短暂的延迟后简单地打印42;然而,在现实中,延迟的时间可能要长很多。它甚至可能永远挂起,或打印出零。

The cause of these anomalies is the lack of proper memory visibility and reordering. Let’s evaluate them in more detail.

这些异常现象的原因是缺乏适当的内存可见性和重新排序。让我们更详细地评估它们。

3.1. Memory Visibility

3.1.内存可见性

In this simple example, we have two application threads: the main thread and the reader thread. Let’s imagine a scenario in which the OS schedules those threads on two different CPU cores, where:

在这个简单的例子中,我们有两个应用线程:主线程和读者线程。让我们设想一个场景,操作系统将这些线程安排在两个不同的CPU核心上,其中。

  • The main thread has its copy of ready and number variables in its core cache.
  • The reader thread ends up with its copies, too.
  • The main thread updates the cached values.

On most modern processors, write requests won’t be applied right after they’re issued. In fact, processors tend to queue those writes in a special write buffer. After a while, they’ll apply those writes to main memory all at once.

在大多数现代处理器上,写请求发出后不会立即被应用。事实上,处理器倾向于在一个特殊的写缓冲区中排队等待这些写操作。一段时间后,他们会将这些写操作一次性应用到主内存。

With all that being said, when the main thread updates the number and ready variables, there’s no guarantee about what the reader thread may see. In other words, the reader thread may see the updated value right away, or with some delay, or never at all.

综上所述,当主线程更新number ready 变量时,并不能保证读者线程会看到什么。换句话说,读者线程可能会立即看到更新的值,或者有一些延迟,或者根本就没有。

This memory visibility may cause liveness issues in programs that are relying on visibility.

这种内存的可见性可能会在依赖可见性的程序中造成有效性问题。

3.2. Reordering

3.2.重新排序

To make matters even worse, the reader thread may see those writes in an order other than the actual program order. For instance, since we first update the number variable:

更糟糕的是,读者线程可能会以不同于实际程序顺序的方式看到这些写入的内容。例如,由于我们首先更新number变量。

public static void main(String[] args) { 
    new Reader().start();
    number = 42; 
    ready = true; 
}

We may expect the reader thread to print 42. But, it’s actually possible to see zero as the printed value.

我们可能期望读者线程打印42。但是,实际上有可能看到打印的数值是0。

The reordering is an optimization technique for performance improvements. Interestingly, different components may apply this optimization:

重新排序是一种改善性能的优化技术。有趣的是,不同的组件可以应用这种优化。

  • The processor may flush its write buffer in an order other than the program order.
  • The processor may apply out-of-order execution technique.
  • The JIT compiler may optimize via reordering.

3.3. volatile Memory Order

3.3.易失性存储器顺序

To ensure that updates to variables propagate predictably to other threads, we should apply the volatile modifier to those variables:

为了确保变量的更新能够可预测地传播到其他线程,我们应该对这些变量应用volatile修饰符:

public class TaskRunner {

    private volatile static int number;
    private volatile static boolean ready;

    // same as before
}

This way, we communicate with runtime and processor to not reorder any instruction involving the volatile variable. Also, processors understand that they should flush any updates to these variables right away.

这样,我们与运行时和处理器沟通,不对涉及volatile变量的任何指令重新排序。同时,处理器也明白,他们应该立即刷新对这些变量的任何更新。

4. volatile and Thread Synchronization

4.volatile和线程同步

For multithreaded applications, we need to ensure a couple of rules for consistent behavior:

对于多线程的应用程序,我们需要确保几个规则的一致性行为。

  • Mutual Exclusion – only one thread executes a critical section at a time
  • Visibility – changes made by one thread to the shared data are visible to other threads to maintain data consistency

synchronized methods and blocks provide both of the above properties at the cost of application performance.

同步方法和块提供了上述两种属性,但却牺牲了应用性能。

volatile is quite a useful keyword because it can help ensure the visibility aspect of the data change without providing mutual exclusion. Thus, it’s useful in the places where we’re ok with multiple threads executing a block of code in parallel, but we need to ensure the visibility property.

volatile是一个相当有用的关键字,因为它可以帮助确保数据变化的可见性而不提供相互排斥。因此,在我们可以接受多个线程并行执行一个代码块,但我们需要确保可见性的地方,它是很有用的。

5. Happens-Before Ordering

5 发生在订货之前

The memory visibility effects of volatile variables extend beyond the volatile variables themselves.

易失性变量的内存可见性影响超出了易失性变量本身。

To make matters more concrete, let’s suppose thread A writes to a volatile variable, and then thread B reads the same volatile variable. In such cases, the values that were visible to A before writing the volatile variable will be visible to B after reading the volatile variable:

为了更具体地说明问题,我们假设线程A向一个volatile变量写入,然后线程B读取同一个volatile变量。在这种情况下,在写入volatile变量之前对A可见的值,在读取volatile变量之后对B可见:

happens before

Technically speaking, any write to a volatile field happens before every subsequent read of the same field. This is the volatile variable rule of the Java Memory Model (JMM).

从技术上讲,对volatile字段的任何写入都发生在同一字段的每个后续读取之前。这就是Java内存模型(JMM)的volatile变量规则。

5.1. Piggybacking

5.1 捎带

Because of the strength of the happens-before memory ordering, sometimes we can piggyback on the visibility properties of another volatile variable. For instance, in our particular example, we just need to mark the ready variable as volatile:

由于 happens-before 内存排序的强度,有时我们可以利用另一个volatile变量的可见性属性。例如,在我们的特定例子中,我们只需要将ready变量标记为volatile

public class TaskRunner {

    private static int number; // not volatile
    private volatile static boolean ready;

    // same as before
}

Anything prior to writing true to the ready variable is visible to anything after reading the ready variable. Therefore, the number variable piggybacks on the memory visibility enforced by the ready variable. Simply puteven though it’s not a volatile variable, it’s exhibiting a volatile behavior.

在向ready变量写入true之前的任何事情,在读取ready变量之后都是可见的。因此,number变量捎带上了ready变量所强制执行的内存可见性。简单地说即使它不是一个易失性变量,它也表现出了易失性行为。

By making use of these semantics, we can define only a few of the variables in our class as volatile and optimize the visibility guarantee.

通过利用这些语义,我们可以在我们的类中只将少数变量定义为volatile ,并优化可见性保证。

6. Conclusion

6.结论

In this article, we explored the volatile keyword and its capabilities, as well as the improvements made to it starting with Java 5.

在这篇文章中,我们探讨了volatile关键词及其功能,以及从Java 5开始对其进行的改进。

As always, the code examples can be found over on GitHub.

一如既往,代码示例可以在GitHub上找到