前语

在没有必要同步的状况下,编译器、运行时或处理器或许会运用各种优化。尽管这些优化通常是有利的,但有时它们会导致一些微妙的问题。 缓存和从头排序是在并发上下文中或许会让咱们感到惊讶的优化之一。 JavaJVM 供给了许多办法来控制内存次序,volatile 关键字便是其中之一。

首要,咱们将从一些有关底层计算机体系结构如何工作的背景知识开端,然后咱们将熟悉 Java 中的内存次序。

同享多处理器架构

处理器担任履行程序指令。因而,他们需求从 RAM 中检索程序指令和所需数据。

由于 CPU 每秒能够履行许多指令,因而从 RAM 中获取数据对它们来说并不理想。为了改善这种状况,处理器正在运用乱序履行、分支猜测、推测履行,当然还有缓存等技巧。

这是以下内存层次结构发挥作用的当地:

关于Volatile你需要知道的一切

跟着不同内核履行更多指令并处理更多数据,它们会用更多相关数据和指令填充缓存。这将以引入缓存一致性挑战为价值进步全体功能。

简而言之,当一个线程更新缓存值时,咱们应该三思而后行。

何时运用 volatile

咱们借用一个示例:

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;
    }
}

TaskRunner 类保护两个简略的变量。在其 main 办法中,它会创立另一个线程,只要 ready 变量为 false,它就会循环。当变量变为真时,线程将简略地打印数字变量。

许多人或许希望这个程序在短暂的延迟后简略地打印 42;然而,实践上,延迟或许要长得多。它乃至或许永久挂起或打印为零。

这些反常的原因是缺少适当的内存可见性和从头排序。让咱们看看哪里出了问题。

内存可见性

在这个简略的示例中,咱们有两个运用程序线程:主线程和读取线程。让咱们幻想一个场景,操作系统在两个不同的 CPU 内核上调度这些线程,其中:

  • 主线程在其中心缓存中有 ready 和 number 变量的副本。
  • 读者线程也以其副本。
  • 主线程更新缓存的值。

在大多数现代处理器上,写入请求不会在发出后立即运用。事实上,处理器倾向于将这些写入排队到一个特别的写入缓冲区中。一段时间后,他们会立行将这些写入运用到主内存。

也便是说, 当主线程更新 number 和 ready 变量时,无法确保读者线程会看到什么。换句话说,读者线程或许会立即看到更新后的值,但有一些延迟,或许根本看不到。

这种内存可见性或许会导致依赖可见性的程序呈现变量不一致的问题。

从头排序

更糟糕的是,读取线程或许会看到这些写入的次序与实践程序次序不同。例如,由于咱们首要更新了数字变量:

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

咱们或许希望读者线程打印 42。但实践上或许会看到0作为打印值。

从头排序是一种用于进步功能的优化技能。有趣的是,不同的组件或许会运用此优化:

  • 处理器或许会以不同于程序次序的次序改写其写入缓冲区。
  • 处理器能够运用乱序履行技能。
  • JIT 编译器能够通过从头排序进行优化。

volatile内存次序

为了确保对变量的更新可猜测地传播到其他线程,咱们应该将 volatile 修饰符运用于这些变量:

public class TaskRunner {
    private volatile static int number;
    private volatile static boolean ready;
    // same as before
}

通过这种办法,咱们告知处理器,不会从头排序任何触及 volatile 变量的指令。此外,处理器明白他们应该立即改写对这些变量的任何更新。

volatile 和线程同步

关于多线程运用程序,咱们需求确保一些规则以实现一致的行为:

  • 互斥——一次只要一个线程履行临界区
  • 可见性——一个线程对同享数据所做的更改对其他线程可见,以保持数据一致性

同步办法和块以运用程序功能为价值供给上述两个特点。

volatile 是一个非常有用的关键字,由于它能够在不供给互斥的状况下协助确保数据更改的可见性。因而,它在咱们能够让多个线程并行履行一段代码但需求确保可见性特点的当地很有用。

Happens-Before 次序

volatile 变量的内存可见性影响超出了 volatile 变量自身。

为了使事情更详细,咱们假设线程 A 写入一个 volatile 变量,然后线程 B 读取同一个 volatile 变量。

在这种状况下,在写入 volatile 变量之前对 A 可见的值将在读取 volatile 变量后对 B 可见:

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

关于Volatile你需要知道的一切

搭便车

由于 happens-before 内存排序的优势,有时咱们能够利用另一个 volatile 变量的可见性特点。例如,在咱们的特定示例中,咱们只需求将安排妥当变量标记为易变的:

public class TaskRunner {
    private static int number; // not volatile
    private volatile static boolean ready;
    // same as before
}

读取安排妥当变量后,任安在写入安排妥当变量之前的内容都对任何内容可见。因而,number 变量搭载在 ready 变量强制履行的内存可见性上。简而言之,即使它不是易变变量,它也表现出易变行为。

运用这些语义,咱们能够仅将类中的几个变量定义为 volatile 并优化可见性确保。