Java 并发编程:volatile 关键字介绍与使用

大家好,我是栗筝i,这篇文章是我的 "栗筝i 的 Java 技术栈" 专栏的第 026 篇文章,在 "栗筝i 的 Java 技术栈" 这个专栏中我会持续为大家更新 Java 技术相关全套技术栈内容。专栏的主要目标是已经有一定 Java 开发经验,并希望进一步完善自己对整个 Java 技术体系来充实自己的技术栈的同学。与此同时,本专栏的所有文章,也都会准备充足的代码示例和完善的知识点梳理,因此也十分适合零基础的小白和要准备工作面试的同学学习。当然,我也会在必要的时候进行相关技术深度的技术解读,相信即使是拥有多年 Java 开发经验的从业者和大佬们也会有所收获并找到乐趣。

--

在现代多线程编程中,确保数据的一致性和正确性是至关重要的。Java 作为一种广泛使用的编程语言,为多线程编程提供了丰富的工具和机制,其中 volatile 关键字是一个关键的概念。volatile 关键字在 Java 中被用来修饰变量,以确保它们在多线程环境下的可见性和有序性,但它并不保证操作的原子性。

理解 volatile 的工作原理及其应用场景,对于编写高效和可靠的多线程程序至关重要。在本文中,我们将深入探讨 volatile 关键字的核心特性,解释它如何确保变量的可见性和有序性,以及它在解决多线程问题中的局限性。我们还将通过示例展示如何在实际编程中使用 volatile,以及如何通过其他同步机制来弥补 volatile 的不足。

通过对 volatile 的详细分析,我们希望读者能够更好地理解在多线程环境中变量访问的复杂性,并掌握在实际开发中如何正确使用 volatile 关键字,以编写出更加健壮和高效的并发程序。


文章目录

      • [1、volatile 关键字简介](#1、volatile 关键字简介)
      • [2、volatile 保证可见性](#2、volatile 保证可见性)
      • [3、volatile 保证有序性](#3、volatile 保证有序性)
      • [4、volatile 不保证原子性的详细介绍](#4、volatile 不保证原子性的详细介绍)

1、volatile 关键字简介

volatile 关键字在 Java 中用于修饰变量,使其具有可见性和有序性。

  • 可见性:在多线程环境下,当一个线程修改了 volatile 变量的值,新值对于其他线程是立即可见的。通常情况下,线程之间对变量的读写操作是不可见的,这意味着一个线程修改了变量的值,另一个线程可能看不到这个修改,仍然使用旧值。使用 volatile 关键字可以确保所有线程看到的是变量的最新值;
  • 有序性:volatile 关键字还可以防止指令重排序优化。编译器和处理器通常会对指令进行重排序,以提高性能,但这种重排序可能会破坏多线程程序的正确性。volatile 变量的读写操作不会被重排序,也不会与前后的读写操作发生重排序。

需要注意的是 volatile 仅能保证可见性和有序性,不能保证原子性。例如,volatile int count 的递增操作 count++ 仍然不是线程安全的,因为它包含了读和写两个操作,可能会被其他线程打断。

在复杂的同步场景中,可能需要使用 synchronized 或其他并发工具来确保线程安全。


2、volatile 保证可见性

在多线程编程中,线程之间共享变量的访问可能会出现可见性问题,即一个线程对变量的修改可能不会被其他线程立即看到。Java 提供了 volatile 关键字来解决这种可见性问题。

2.1、什么是可见性问题

当一个线程修改了某个变量的值,如果这个修改对其他线程是不可见的,可能会导致程序出现非预期的行为。例如,一个线程修改了变量 flag 的值,但其他线程仍然读取的是旧值:

java 复制代码
public class VisibilityProblem {
    private boolean flag = true;

    public void stop() {
        flag = false;
    }

    public void run() {
        while (flag) {
            // 执行任务
        }
    }
}

在这个例子中,如果 flag 变量没有被声明为 volatile,当一个线程调用 stop 方法将 flag 设置为 false 后,另一个正在运行 run 方法的线程可能无法立即看到这个变化,仍然会在 while (flag) 循环中继续执行。

2.2、volatile 如何保证可见性

volatile 关键字通过以下机制确保变量的可见性:

  1. 内存可见性协议:

    • 每个线程都有自己的本地缓存,当一个线程对变量进行读写操作时,实际上是从本地缓存中读取或写入的,而不是直接操作主内存中的变量。
    • 当一个变量被声明为 volatile 时,所有线程对该变量的读写操作都将直接操作主内存,而不是使用本地缓存。
    • 当一个线程修改了 volatile 变量的值,这个新值会立即刷新到主内存中。
    • 任何线程在读取 volatile 变量时,都会从主内存中读取最新的值,而不是从本地缓存中读取旧值。
  2. 内存屏障:

    • volatile 关键字在底层实现中,会在变量的读写操作前后插入内存屏障(Memory Barrier)。
    • 内存屏障确保了指令的执行顺序,防止编译器和处理器对 volatile 变量的读写操作进行重排序。
    • 写内存屏障:确保在写 volatile 变量之前的所有写操作都已经完成,并且结果对其他线程可见。
    • 读内存屏障:确保在读 volatile 变量之后的所有读操作都能读取到最新的值。

示例代码:

java 复制代码
public class VolatileExample {
    private volatile boolean running = true;

    public void stop() {
        running = false;
    }

    public void run() {
        while (running) {
            // 执行任务
        }
    }

    public static void main(String[] args) {
        VolatileExample example = new VolatileExample();
        Thread thread = new Thread(example::run);
        thread.start();

        try {
            Thread.sleep(1000); // 让线程运行一段时间
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        example.stop(); // 停止线程
    }
}

在这个例子中,running 变量被声明为 volatile,确保 stop 方法对 running 的修改能够立即被 run 方法中的循环检测到。


3、volatile 保证有序性

在多线程编程中,指令重排序(Instruction Reordering)可能会导致程序的执行顺序与代码的书写顺序不一致,从而引发不可预测的问题。volatile 关键字通过内存屏障(Memory Barrier)机制,防止指令重排序,确保代码执行的有序性。

3.1、什么是指令重排序

为了优化程序的执行速度,编译器和处理器会对指令进行重排序。重排序包括以下三种类型:

  1. 编译器重排序:编译器在生成机器指令时,可以重新安排代码的执行顺序。
  2. 处理器重排序:处理器可以在运行时对指令进行重排序,以充分利用处理器流水线。
  3. 内存系统重排序:由于缓存、写缓冲区等原因,内存操作的顺序可能与程序代码的顺序不同。

尽管重排序不会改变单线程程序的语义,但在多线程环境下,重排序可能会导致线程间的操作顺序不一致,从而引发数据竞争和线程安全问题。

3.2、volatile 如何保证有序性

volatile 关键字通过插入内存屏障,确保指令的执行顺序。内存屏障是一种同步机制,防止特定类型的指令在重排序时被移动到屏障的另一侧。volatile 变量的读写操作前后会插入内存屏障,确保有序性:

  1. 写内存屏障(Store Barrier):在写 volatile 变量之前插入,确保在此屏障之前的所有写操作都已完成,并且结果对其他线程可见;
  2. 读内存屏障(Load Barrier):在读 volatile 变量之后插入,确保在此屏障之后的所有读操作能读取到最新的值。

具体而言,volatile 保证了以下两点:

  1. volatile 变量之前的所有写操作不会被重排序到 volatile 写之后;
  2. volatile 变量之后的所有读操作不会被重排序到 volatile 读之前。

示例代码:

java 复制代码
public class VolatileOrderingExample {
    private volatile boolean flag = false;
    private int a = 0;

    public void writer() {
        a = 1;         // 写普通变量
        flag = true;   // 写volatile变量
    }

    public void reader() {
        if (flag) {    // 读volatile变量
            int i = a; // 读普通变量
            // `i` 将是 1,因为 `flag` 为 true 时,`a` 必定已经被写为 1
        }
    }
}

在这个例子中,writer 方法中对 a 的写操作不会被重排序到 flag 之后,因此在 reader 方法中,一旦检测到 flagtrue,就能确保读取到的 a 的值是最新的 1


4、volatile 不保证原子性的详细介绍

在多线程编程中,volatile 关键字可以保证变量的可见性和有序性,但不能保证操作的原子性。原子性(Atomicity)指的是操作在执行过程中不可分割,要么全部执行,要么全部不执行。

4.1、什么是原子性问题

在多线程环境下,非原子操作可能会导致数据不一致。例如,自增操作 i++ 看似简单,但它实际上由三步组成:

  1. 读取变量 i 的当前值;
  2. i 的值加 1;
  3. 将新值写回 i

这三步操作在多线程环境下可能会被打断,从而导致数据竞争问题。假设两个线程同时执行 i++ 操作:

  1. 线程 A 读取 i 的值为 5。
  2. 线程 B 读取 i 的值为 5。
  3. 线程 A 将 i 的值加 1 并写回,i 的值变为 6。
  4. 线程 B 将 i 的值加 1 并写回,i 的值变为 6。

最终结果是,虽然两个线程都执行了 i++ 操作,但 i 的值只增加了 1。这就是因为 i++ 操作不是原子的。

4.2、volatile 的局限性

volatile 仅能确保变量的可见性和有序性,但不能确保操作的原子性。换句话说,使用 volatile 修饰的变量虽然可以在多个线程之间及时同步,但多个线程对该变量的复合操作(如自增、自减)仍然会存在数据竞争问题。

以下是一个例子,说明了 volatile 不保证原子性的问题:

java 复制代码
public class VolatileNonAtomic {
    private volatile int count = 0;

    public void increment() {
        count++;
    }

    public static void main(String[] args) throws InterruptedException {
        VolatileNonAtomic example = new VolatileNonAtomic();

        Runnable task = () -> {
            for (int i = 0; i < 1000; i++) {
                example.increment();
            }
        };

        Thread t1 = new Thread(task);
        Thread t2 = new Thread(task);

        t1.start();
        t2.start();

        t1.join();
        t2.join();

        System.out.println("Final count: " + example.count);
    }
}

在这个例子中,尽管 count 变量被声明为 volatile,但由于 increment 方法中的 count++ 操作不是原子的,最终的 count 值可能小于 2000。

4.3、解决方法

为了确保操作的原子性,可以使用以下方法:

  1. 使用 synchronized 关键字:将操作包装在同步块中,确保操作的原子性。

    java 复制代码
    public class SynchronizedExample {
        private int count = 0;
    
        public synchronized void increment() {
            count++;
        }
    }
  2. 使用原子类:Java 提供了 java.util.concurrent.atomic 包中的原子类(如 AtomicIntegerAtomicLong)来确保操作的原子性。

    java 复制代码
    import java.util.concurrent.atomic.AtomicInteger;
    
    public class AtomicExample {
        private AtomicInteger count = new AtomicInteger(0);
    
        public void increment() {
            count.incrementAndGet();
        }
    }
相关推荐
编程火箭车6 天前
【Java SE 基础学习打卡】36 数组的常见操作
冒泡排序·java se·java 基础·线性查找·数组常见操作·数组增删改·数组拷贝
编程火箭车7 天前
【Java SE 基础学习打卡】35 数组元素的访问与遍历
数组索引·java se·java 基础·java 数组·数组元素访问与遍历·普通 for 循环·增强 for 循环
编程火箭车11 天前
【Java SE 基础学习打卡】34 数组的定义与初始化
java se·java 基础·java 数组·数组定义与初始化·静态初始化·动态初始化·length 属性
编程火箭车12 天前
【Java SE 基础学习打卡】33 数组的概述
java se·java 基础·数组概述·数组核心特征·java 数组·批量存储数据·连续内存存储
编程火箭车13 天前
【Java SE 基础学习打卡】32 方法的嵌套调用与递归调用
java se·java 基础·java 方法·方法嵌套调用·方法递归调用·递归终止条件·递归应用场景
编程火箭车13 天前
【Java SE 基础学习打卡】31 方法的返回值与void关键字
java se·java 基础·return 语句·编程小白入门·java 方法·方法返回值·void 关键字
编程火箭车16 天前
【Java SE 基础学习打卡】28 方法的定义与调用
java se·参数传递·返回值·java 基础·新手避坑·java 方法·方法定义与调用
切糕师学AI2 个月前
ARM 架构中的数据内存屏障指令 DMB
arm开发·架构·指令·内存屏障
叶 落8 个月前
[Java 基础]正则表达式
java·正则表达式·java 基础
叶 落8 个月前
[Java 基础]数组
java·java 基础