admin管理员组

文章数量:1441114

Volatile关键字使用指南

1. 概述

在没有必要同步的情况下,编译器、运行时或处理器可能会应用各种优化。尽管这些优化通常是有益的,但有时它们可能会导致微妙的问题。

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

在本教程中,我们将重点介绍 Java 语言中基本但经常被误解的概念,即 volatile关键字。首先,我们将从有关底层计算机体系结构如何工作的一些背景知识开始,然后我们将熟悉 Java 中的内存顺序。

2. 共享多处理器架构

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

由于 CPU 每秒可以执行许多指令,因此从 RAM 获取对它们来说并不理想。为了改善这种情况,处理器正在使用诸如乱序执行,分支预测,推测执行等技巧,当然还有缓存。

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

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

简而言之,我们应该三思而后行,当一个线程更新缓存值时会发生什么。

3. 何时使用volatile

为了进一步扩展缓存一致性,我们将借用《Java Concurrency in Practice》一书中的一个例子:

代码语言:javascript代码运行次数:0运行复制
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;
    }
}Copy

TaskRunner类维护两个简单的变量。在它的main方法中,它创建了另一个线程,只要ready变量为false,该线程就在ready变量上自旋转(就绪状态)。当变量为true时,线程将简单地打印number变量。

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

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

3.1. 内存可见性

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

  • 主线程在其核心缓存中具有ready变量和number变量的副本。
  • 阅读器线程最终也会得到它的副本。
  • 主线程更新缓存的值。

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

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

此内存可见性可能会导致依赖于可见性的程序出现活动问题。

3.2. 重新排序

更糟糕的是,读线程看到的写入顺序可能与实际的程序顺序不同。例如,因为我们首先更新了number变量:

代码语言:javascript代码运行次数:0运行复制
public static void main(String[] args) { 
    new Reader().start();
    number = 42; 
    ready = true; 
}Copy

我们可能期望读取器线程打印 42。但实际上可以将零视为打印值。

重新排序是一种用于改进性能的优化技术。有趣的是,不同的组件可能会应用此优化:

  • 处理器可以按程序顺序以外的顺序刷新其写入缓冲区。
  • 处理器可以应用无序执行技术。
  • JIT 编译器可以通过重新排序进行优化。

3.3.volatile内存顺序

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

代码语言:javascript代码运行次数:0运行复制
public class TaskRunner {

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

    // same as before
}Copy

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

4.volatile和线程同步

对于多线程应用程序,我们需要确保一些规则来实现一致的行为:

  • 互斥 – 一次只有一个线程执行关键部分
  • 可见性 – 一个线程对共享数据所做的更改对其他线程可见,以保持数据一致性

同步方法和块以牺牲应用程序性能为代价提供上述两个属性。

volatile是一个非常有用的关键字,因为它可以帮助确保数据更改的可见性方面,而无需提供互斥。因此,在我们可以接受多个线程并行执行代码块的地方,它很有用,但我们需要确保可见性属性。

5. 发生在排序前

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

为了使事情更具体,假设线程 A 写入一个易失变量,然后线程 B 读取相同的volatile变量。在这种情况下,在写入volatile变量之前对 A 可见的值在读取volatile变量后对 B 可见:

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

5.1. 背负式生产

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

代码语言:javascript代码运行次数:0运行复制
public class TaskRunner {

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

    // same as before
}Copy

true写入ready变量之前的任何内容对读取ready变量后的任何内容都是可见的。因此,number变量搭载在ready变量强制执行的内存可见性上。简而言之,即使它不是一个volatile变量,它也表现出一种volatile的行为。

使用这些语义,我们只能将类中的几个变量定义为volatile变量,并优化可见性保证。

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。 原始发表:2023-02-22,如有侵权请联系 cloudcommunity@tencent 删除教程线程javavolatile变量

本文标签: Volatile关键字使用指南