当前位置: 首页 > news >正文

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

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

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

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

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


文章目录

      • 1、volatile 关键字简介
      • 2、volatile 保证可见性
        • 2.1、什么是可见性问题
        • 2.2、volatile 如何保证可见性
      • 3、volatile 保证有序性
        • 3.1、什么是指令重排序
        • 3.2、volatile 如何保证有序性
      • 4、volatile 不保证原子性的详细介绍
        • 4.1、什么是原子性问题
        • 4.2、volatile 的局限性
        • 4.3、解决方法


1、volatile 关键字简介

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

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

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

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


2、volatile 保证可见性

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

2.1、什么是可见性问题

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

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 变量之后的所有读操作都能读取到最新的值。

示例代码:

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 读之前。

示例代码:

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 不保证原子性的问题:

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 关键字:将操作包装在同步块中,确保操作的原子性。

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

    import java.util.concurrent.atomic.AtomicInteger;public class AtomicExample {private AtomicInteger count = new AtomicInteger(0);public void increment() {count.incrementAndGet();}
    }
    

相关文章:

  • 北京网站建设多少钱?
  • 辽宁网页制作哪家好_网站建设
  • 高端品牌网站建设_汉中网站制作
  • 【系统架构设计师】二十四、安全架构设计理论与实践④
  • 安装ubuntu server24.04系统
  • 浅谈 Spring AOP框架 (1)
  • 数据湖之Hudi
  • Java 技巧:将整数每一位数字转换为数组
  • 【C++题解】1015. 鸡兔同笼问题
  • ABAQUS基于CT断层扫描的三维圆柱体多孔结构建模
  • 【LabVIEW学习篇 - 12】:通知器
  • 数据挖掘可以挖掘什么类型的模式?
  • ReentrantLock源码分析
  • QChart曲线绘制-1.普通曲线
  • 数据结构——双链表详解(超详细)
  • git学习入门1——下载安装与添加用户标识设置name与Email
  • 音频重采样基本流程
  • MybatisPlus对象注释规则笔记
  • 【许晓笛】 EOS 智能合约案例解析(3)
  • Angularjs之国际化
  • angular学习第一篇-----环境搭建
  • es的写入过程
  • js递归,无限分级树形折叠菜单
  • Spring Security中异常上抛机制及对于转型处理的一些感悟
  • Vue学习第二天
  • Work@Alibaba 阿里巴巴的企业应用构建之路
  • 个人博客开发系列:评论功能之GitHub账号OAuth授权
  • 深度学习在携程攻略社区的应用
  • 网页视频流m3u8/ts视频下载
  • 微信公众号开发小记——5.python微信红包
  • puppet连载22:define用法
  • 树莓派用上kodexplorer也能玩成私有网盘
  • ​【原创】基于SSM的酒店预约管理系统(酒店管理系统毕业设计)
  • ​iOS实时查看App运行日志
  • #laravel部署安装报错loadFactoriesFrom是undefined method #
  • #Linux(帮助手册)
  • #Z2294. 打印树的直径
  • (1)svelte 教程:hello world
  • (13)DroneCAN 适配器节点(一)
  • (26)4.7 字符函数和字符串函数
  • (CPU/GPU)粒子继承贴图颜色发射
  • (el-Date-Picker)操作(不使用 ts):Element-plus 中 DatePicker 组件的使用及输出想要日期格式需求的解决过程
  • (附源码)python旅游推荐系统 毕业设计 250623
  • (附源码)ssm高校实验室 毕业设计 800008
  • (论文阅读22/100)Learning a Deep Compact Image Representation for Visual Tracking
  • (免费领源码)Java#Springboot#mysql农产品销售管理系统47627-计算机毕业设计项目选题推荐
  • (已更新)关于Visual Studio 2019安装时VS installer无法下载文件,进度条为0,显示网络有问题的解决办法
  • (转)母版页和相对路径
  • *p++,*(p++),*++p,(*p)++区别?
  • .mat 文件的加载与创建 矩阵变图像? ∈ Matlab 使用笔记
  • .NET BackgroundWorker
  • .NET 发展历程
  • .NET/C# 使窗口永不激活(No Activate 永不获得焦点)
  • .NET/C# 中设置当发生某个特定异常时进入断点(不借助 Visual Studio 的纯代码实现)
  • .NET企业级应用架构设计系列之结尾篇
  • /bin、/sbin、/usr/bin、/usr/sbin
  • @ResponseBody
  • [ 常用工具篇 ] POC-bomber 漏洞检测工具安装及使用详解