Java并发编程实战 Day 3:volatile关键字与内存可见性
【Java并发编程实战 Day 3】volatile关键字与内存可见性
开篇
欢迎来到《Java并发编程实战》系列的第3天!本系列旨在带领你从基础到高级逐步掌握Java并发编程的核心概念和最佳实践。
今天我们将重点探讨volatile
关键字及其在多线程程序中确保内存可见性的作用。我们会从JVM层面解释其工作原理,展示如何在业务场景中使用它来避免线程间的数据不一致问题,并通过完整的Java代码示例进行演示。
理论基础:volatile关键字与内存模型
Java内存模型(JMM)概述
Java内存模型(Java Memory Model, JMM)定义了Java程序中变量的访问规则,屏蔽了不同硬件平台和操作系统的差异,保证了Java程序在各种平台下对内存的访问效果一致。
在JMM中,每个线程都有自己的本地内存(Local Memory),其中保存了主内存(Main Memory)中该线程使用的变量副本。线程读写变量时,默认情况下只能访问本地内存,这可能导致多个线程看到的变量值不一致。
volatile的语义
volatile
是Java中用于修饰变量的关键字,具有以下特性:
- 可见性:当一个线程修改了一个volatile变量的值后,新值对其他线程来说是立即可见的。
- 有序性:禁止指令重排序优化,即编译器和处理器不会对volatile变量的读/写操作进行重排序。
需要注意的是,虽然volatile
可以保证可见性和有序性,但它不能保证原子性。例如,对volatile变量的自增操作不是原子的。
volatile的工作机制
为了实现上述特性,JMM为volatile变量引入了两条规则:
- 写入volatile变量时,会插入一个写屏障(Store Barrier),强制将本地内存中的最新值刷新到主内存。
- 读取volatile变量时,会插入一个读屏障(Load Barrier),强制从主内存中重新加载该变量的值。
此外,在JIT编译阶段,编译器会对volatile变量的操作添加额外的限制,防止出现指令重排。
适用场景:何时使用volatile
volatile
适用于以下几种情况:
- 状态标志:如控制线程启动或停止的状态变量。
- 一次性安全发布:如初始化完成后共享不可变对象。
- 双重检查锁定(DCL):用于单例模式中延迟初始化。
- 计数器(非原子操作):如简单的布尔开关。
但要注意,对于复合操作(如i++),应使用synchronized
或AtomicInteger
等具备原子性的工具类。
代码实践:volatile的使用示例
我们来看一个简单的例子,演示如何使用volatile
来实现线程间的通信。
/*** 使用volatile实现线程间通信的简单示例*/
public class VolatileExample {// 使用volatile修饰变量,确保可见性private static volatile boolean isRunning = true;public static void main(String[] args) {Thread workerThread = new Thread(() -> {int count = 0;while (isRunning) {count++;System.out.println("Worker thread is running..." + count);try {Thread.sleep(500);} catch (InterruptedException e) {Thread.currentThread().interrupt();break;}}System.out.println("Worker thread stopped.");});workerThread.start();// 主线程等待一段时间后修改isRunning为falsetry {Thread.sleep(3000);} catch (InterruptedException e) {Thread.currentThread().interrupt();}System.out.println("Stopping worker thread...");isRunning = false;}
}
在这个例子中,主线程启动一个工作线程,并让其持续运行直到isRunning
被设置为false
。由于isRunning
被声明为volatile
,因此主线程对其的修改能立即被工作线程感知。
如果我们去掉volatile
关键字,工作线程可能永远看不到isRunning
的变化,导致死循环。
实现原理:volatile背后的JVM机制
字节码与JIT编译
当我们使用volatile
修饰一个变量时,JVM会在生成的字节码中对该变量的访问做出特殊处理。以如下代码为例:
private static volatile int counter = 0;public static void increment() {counter++;
}
反编译得到的字节码如下:
public static void increment();Code:0: getstatic #2 // Field counter:I3: iconst_14: iadd5: putstatic #2 // Field counter:I8: return
虽然字节码看起来与普通变量一样,但在运行时,JVM会根据变量是否为volatile
来决定是否插入内存屏障。
内存屏障(Memory Barrier)
内存屏障是一组CPU指令,用来控制指令顺序和内存访问顺序。JVM在编译volatile变量的读写操作时会插入特定类型的内存屏障:
操作类型 | 插入的内存屏障 |
---|---|
volatile写前 | StoreStore屏障 |
volatile写后 | StoreLoad屏障 |
volatile读前 | LoadLoad屏障 |
volatile读后 | LoadStore屏障 |
这些屏障的作用是防止编译器和处理器对指令进行重排序,同时确保数据的可见性。
CPU缓存一致性协议
现代CPU通常采用MESI协议来维护缓存一致性。当一个线程修改了一个volatile变量时,该变量所在的缓存行会被标记为“已修改”,并通过总线广播通知其他核心,使其本地缓存失效,从而确保所有线程都能读取到最新的值。
性能测试:volatile vs 非volatile变量
下面我们通过一个简单的性能测试来比较volatile变量与非volatile变量的性能差异。
/*** 性能测试:volatile与非volatile变量对比*/
public class VolatilePerformanceTest {private static volatile int volatileCounter = 0;private static int nonVolatileCounter = 0;public static void main(String[] args) throws InterruptedException {int iterations = 100_000_000;// 测试volatile变量long start = System.currentTimeMillis();for (int i = 0; i < iterations; i++) {volatileCounter++;}long end = System.currentTimeMillis();System.out.println("Volatile variable took " + (end - start) + " ms");// 测试非volatile变量start = System.currentTimeMillis();for (int i = 0; i < iterations; i++) {nonVolatileCounter++;}end = System.currentTimeMillis();System.out.println("Non-volatile variable took " + (end - start) + " ms");}
}
测试结果如下(因机器配置不同而异):
Volatile variable took 2500 ms
Non-volatile variable took 1000 ms
可以看到,volatile变量的性能明显低于非volatile变量,这是因为每次写操作都需要刷新主内存并插入内存屏障,增加了开销。
多线程环境下的性能对比
我们再来看看在多线程环境下volatile的表现:
/*** 多线程环境下volatile性能测试*/
public class MultiThreadedVolatileTest {private static volatile int volatileCounter = 0;private static final int THREAD_COUNT = 4;private static final int ITERATIONS = 10_000_000;public static void main(String[] args) throws InterruptedException {Thread[] threads = new Thread[THREAD_COUNT];// 启动多个线程递增volatile变量for (int i = 0; i < THREAD_COUNT; i++) {threads[i] = new Thread(() -> {for (int j = 0; j < ITERATIONS; j++) {volatileCounter++;}});threads[i].start();}// 等待所有线程完成for (Thread t : threads) {t.join();}System.out.println("Final volatileCounter value: " + volatileCounter);}
}
测试结果:
Final volatileCounter value: 39999996
由于没有加锁,volatileCounter的最终值并不等于预期的40000000,说明volatile无法保证原子性。若要保证原子性,应使用AtomicInteger
。
最佳实践:如何正确使用volatile
推荐使用方式
- 仅用于状态标志:如控制线程启停的布尔变量。
- 配合final一起使用:用于初始化后不再更改的对象引用。
- 避免用于计数器:应使用
AtomicInteger
或synchronized
。 - 不要替代synchronized:两者解决的问题不同,volatile只保证可见性,不保证原子性。
注意事项
volatile
不能替代锁,不能保证复合操作的原子性。- 在Java 5之前,volatile的行为不稳定,建议使用Java 5及以上版本。
volatile
变量的性能代价较高,应在必要时才使用。
案例分析:volatile在生产环境中的应用
案例背景
某电商平台在高并发下单系统中遇到一个问题:后台服务需要根据商品库存动态调整是否接受订单。库存信息由独立的服务定时更新,但在某些情况下,订单线程未能及时感知库存变化,导致超卖。
解决方案
我们将库存状态变量声明为volatile
,并在订单创建逻辑中定期检查该变量的值。
/*** 库存状态监控示例*/
public class InventoryService {// 使用volatile确保库存状态实时可见private static volatile boolean isStockAvailable = true;// 模拟库存更新服务public static void updateInventory(boolean available) {isStockAvailable = available;System.out.println("Inventory status updated to: " + isStockAvailable);}// 订单创建逻辑public static void createOrderIfPossible() {if (isStockAvailable) {System.out.println("Creating order...");// 实际创建订单逻辑} else {System.out.println("Out of stock, order rejected.");}}public static void main(String[] args) {// 启动模拟库存更新线程new Thread(() -> {try {Thread.sleep(3000);updateInventory(false);} catch (InterruptedException e) {Thread.currentThread().interrupt();}}).start();// 模拟订单请求for (int i = 0; i < 10; i++) {new Thread(() -> {try {Thread.sleep(1000);createOrderIfPossible();} catch (InterruptedException e) {Thread.currentThread().interrupt();}}).start();}}
}
在这个案例中,通过使用volatile
,订单线程能够及时感知库存变化,从而避免超卖现象的发生。
总结
今天我们学习了Java中volatile
关键字的使用方法及其背后的工作机制。我们了解了JMM的基本概念、volatile的语义、适用场景以及其实现原理。通过实际的代码示例和性能测试,我们验证了volatile在多线程环境下的作用和局限性。
核心技能总结
- 理解Java内存模型(JMM)的基本原理
- 掌握volatile关键字的可见性和有序性保障
- 能够判断何时使用volatile以及何时应选择其他同步机制
- 了解volatile背后的JVM实现机制,包括内存屏障和缓存一致性
- 能够在实际项目中合理使用volatile解决线程通信问题
下一天预告
明天我们将继续深入Java并发编程,介绍线程间通信机制,包括wait/notify
、Condition
、CountDownLatch
等重要工具类。敬请期待!
参考资料
- Java Language Specification - Chapter 17
- Understanding the Java Memory Model
- Oracle官方文档:Java SE Concurrency Utilities
- 《Java并发编程实战》书籍
- Java volatile keyword explained
相关文章:
Java并发编程实战 Day 3:volatile关键字与内存可见性
【Java并发编程实战 Day 3】volatile关键字与内存可见性 开篇 欢迎来到《Java并发编程实战》系列的第3天!本系列旨在带领你从基础到高级逐步掌握Java并发编程的核心概念和最佳实践。 今天我们将重点探讨volatile关键字及其在多线程程序中确保内存可见性的作用。我…...

华为OD机试真题——报文回路(2025A卷:100分)Java/python/JavaScript/C/C++/GO最佳实现
2025 A卷 100分 题型 本专栏内全部题目均提供Java、python、JavaScript、C、C++、GO六种语言的最佳实现方式; 并且每种语言均涵盖详细的问题分析、解题思路、代码实现、代码详解、3个测试用例以及综合分析; 本文收录于专栏:《2025华为OD真题目录+全流程解析+备考攻略+经验分…...
K8s工作流程与YAML实用指南
K8s 工作流程 K8s 采用声明式管理(用户说"要什么",K8s 负责"怎么做")方式,通过 YAML 文件描述期望的状态,K8s控制平面会自动确保实际状态与期望状态一致。 核心工作流程如下: 用户提交…...

功能丰富的PDF处理免费软件推荐
软件介绍 今天给大家介绍一款超棒的PDF工具箱,它处理PDF文档的能力超强,而且是完全免费使用的,没有任何限制。 TinyTools(PC)这款软件,下载完成后即可直接打开使用。在使用过程中,操作完毕后&a…...

Java补充(Java8新特性)(和IO都很重要)
一、Lambda表达式 1.1、为什么使用Lambda表达式 Lambda表达式起步案例 下面源码注释是传统写法,代码是简写表达式写法 import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.function.Consumer;/* * 学什么…...
pycharm debug的时候无法debug到指定的位置就停住不动了
报错大致是这样的,但是直接run没有问题,debug就停住不动了 Traceback (most recent call last): File "/home/mapengsen/.pycharm_helpers/pydev/_pydevd_bundle/pydevd_comm.py", line 467, in start_client s.connect((host, port)) Timeou…...

分布式流处理与消息传递——Kafka ISR(In-Sync Replicas)算法深度解析
Java Kafka ISR(In-Sync Replicas)算法深度解析 一、ISR核心原理 #mermaid-svg-OQtnaUGNQ9PMgbW0 {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg-OQtnaUGNQ9PMgbW0 .error-icon{fill:#55222…...
极大似然估计例题——正态分布的极大似然估计
设总体 X ∼ N ( μ , σ 2 ) X \sim N(\mu, \sigma^2) X∼N(μ,σ2),其中 μ \mu μ 和 σ 2 \sigma^2 σ2 是未知参数,取样本观测值为 x 1 , x 2 , ⋯ , x n x_1, x_2, \cdots, x_n x1,x2,⋯,xn,求参数 μ \mu μ 和 σ 2 \sigma^2 σ…...
Pull Request Integration 拉取请求集成
今天我想要把我创建的项目,通过修改yaml里面的内容,让我在main分支下的其他分支拉取请求的时候自动化测试拉取的内容,以及将测试结果上传到控制台云端。 首先我通过修改yaml文件里面的内容 name: Build and Teston:push:branches:- mainjobs:…...

OS10.【Linux】yum命令
目录 1.安装软件的几种方法 直接编译源代码,得到可执行程序 使用软件包管理器 2.yum yum list命令 参数解释 yum install命令 yum remove命令 下载链接存放的位置 扩展yum源 实验:安装sl小火车命令 sl命令的选项 方法1:man sl 方法2:读源代码 3.更新yum源 查看…...
头歌数据库课程实验(角色管理)
第1关:创建角色 任务描述 本关任务:创建角色 role1localhost。 相关知识 为了完成本关任务,你需要掌握MySQL的角色管理。 角色信息存放在数据库 mysql 的 user 表中。 user 表中字段: Host:可以登陆数据库的主机地…...
【android bluetooth 协议分析 03】【蓝牙扫描详解 1】【扫描关键函数 btif_dm_search_devices_evt 分析】
1. 背景 本篇我们来对 btif_dm_search_devices_evt 函数进行分析. 这是系统性分析 Bluetooth 协议栈中的设备扫描流程时必须厘清的一环。 1. 为什么要单独分析 btif_dm_search_devices_evt 函数: btif_dm_search_devices_evt 是 BTIF 层中处理设备扫描࿰…...
SpringBoot使用ThreadLocal保存登录用户信息
Java 多线程,系列文章: 《Java多线程》 《Java创建多线程的3种方法:继承Thread类、实现Runnable接口、实现Callable接口》 《Java多线程的同步:synchronized关键字、Lock接口、volatile关键字》 《Java线程池》 《Java线程池实现秒杀功能》 《SpringBoot使用ThreadLocal保存…...

多模态大语言模型arxiv论文略读(102)
Chat2Layout: Interactive 3D Furniture Layout with a Multimodal LLM ➡️ 论文标题:Chat2Layout: Interactive 3D Furniture Layout with a Multimodal LLM ➡️ 论文作者:Can Wang, Hongliang Zhong, Menglei Chai, Mingming He, Dongdong Chen, Ji…...
Ubuntu系统如何部署Crawlab爬虫管理平台(通过docker部署)
Ubuntu系统如何部署Crawlab爬虫管理平台(通过docker部署) 一、安装docker(ubuntu系统版本20.4) 1、更新apt sudo apt-get update2、安装必要的依赖包 sudo apt-get install ca-certificates curl gnupg lsb-release3、添加 Docker 官方 GPG 密钥(清化大学源) # 添加Docke…...
python常用库-pandas、Hugging Face的datasets库(大模型之JSONL(JSON Lines))
文章目录 python常用库pandas、Hugging Face的datasets库(大模型之JSONL(JSON Lines))背景什么是JSONL(JSON Lines)通过pandas读取和保存JSONL文件pandas读取和保存JSONL文件 Hugging Face的datasets库Hugg…...

高端装备制造企业如何选择适配的项目管理系统提升项目执行效率?附选型案例
高端装备制造项目通常涉及多专业协同、长周期交付和高风险管控,因此系统需具备全生命周期管理能力。例如,北京奥博思公司出品的 PowerProject 项目管理系统就是一款非常适合制造企业使用的项目管理软件系统。 国内某大型半导体装备制造企业与奥博思软件达…...
【Dv3Admin】工具权限配置文件解析
接口级权限控制是后台系统安全防护的核心手段。基于用户角色、请求路径与方法进行细粒度授权,可以有效隔离不同用户的数据访问范围,防止越权操作,保障系统整体稳定性。 本文解析 dvadmin/utils/permission.py 模块,重点关注其在匿…...

AI炼丹日志-22 - MCP 自动操作 Figma+Cursor 自动设计原型
MCP 基本介绍 官方地址: https://modelcontextprotocol.io/introduction “MCP 是一种开放协议,旨在标准化应用程序向大型语言模型(LLM)提供上下文的方式。可以把 MCP 想象成 AI 应用程序的 USB-C 接口。就像 USB-C 提供了一种…...
Python爬虫:AutoScraper 库详细使用大全(一个智能、自动、轻量级的网络爬虫)
更多内容请见: 爬虫和逆向教程-专栏介绍和目录 文章目录 一、AutoScraper概述1.1 AutoScraper介绍1.2 安装1.3 注意事项二、基本使用方法2.1 创建 AutoScraper 实例2.2 训练模型2.3 保存和加载模型2.4 数据提取方法2.5 自定义规则三、高级功能3.1 多规则抓取3.2 分页抓取3.3 代…...
2025.6.1总结
今天又上了一天课,假期三天,上了两天的课,明天还得刷题。利用假期时间上课学习,并没有让我感到有多充实,反而让我感到有些小压抑。 在下午的好消息分享环节,我分享了毕业工作以来的一些迷茫。我不知道自己…...

[嵌入式实验]实验四:串口打印电压及温度
一、实验目的 熟悉开发环境在开发板上读取电压和温度信息使用串口和PC通信在PC上输出当前电压和温度信息 二、实验环境 硬件:STM32开发板、CMSIS-DAP调试工具 软件:STM32CubeMX软件、ARM的IDE:Keil C51 三、实验内容 配置相关硬件设施 &…...
LVS+Keepalived 高可用
目录 一、核心概念 1. LVS(Linux Virtual Server) 2. Keepalived 二、高可用架构设计 1. 架构拓扑图 2. 工作流程 三、部署步骤(以 DR 模式为例) 1. 环境准备 2. 主 LVS 节点配置 (1)安装 Keepali…...

Linux正则三剑客篇
一、历史命令 history 命令 :用于输出历史上使用过的命令行数量及具体命令。通过 history 可以快速查看并回顾之前执行过的命令,方便重复操作或追溯执行过程。 !行号 :通过指定历史命令的行号来重新执行该行号对应的命令。例如,若…...
HTML5 视频播放器:从基础到进阶的实现指南
在现代Web开发中,视频播放功能是许多网站的重要组成部分。无论是在线教育平台、视频分享网站,还是企业官网,HTML5视频播放器都扮演着不可或缺的角色。本文将从基础到进阶,详细介绍如何实现一个功能完善的HTML5视频播放器ÿ…...
鸿蒙HarmonyOS (React Native)的实战教程
一、环境配置 安装鸿蒙专属模板 bashCopy Code npx react-native0.72.5 init HarmonyApp --template react-native-template-harmony:ml-citation{ref"4,6" data"citationList"} 配置 ArkTS 模块路径 在 entry/src/main/ets 目录下创建原生模块&…...
函数栈帧深度解析:从寄存器操作看函数调用机制
文章目录 一、程序运行的 "舞台":内存栈区与核心寄存器二、寄存器在函数调用中的核心作用三、函数调用全流程解析:以 main 调用 func 为例阶段 1:main 函数栈帧初始化**阶段 2:参数压栈(右→左顺序&#x…...

【计算机网络】第3章:传输层—可靠数据传输的原理
目录 一、PPT 二、总结 (一)可靠数据传输原理 关键机制 1. 序号机制 (Sequence Numbers) 2. 确认机制 (Acknowledgements - ACKs) 3. 重传机制 (Retransmission) 4. 校验和 (Checksum) 5. 流量控制 (Flow Control) 协议实现的核心:滑…...
rv1126b sdk移植
DDR rkbin bin/rv11/rv1126bp_ddr_v1.00.bin v1.00 板子2 reboot异常 [ 90.334976] reboot:Restarting system DDR 950804cb85 wesley.yao 25/04/02-15:54:40,fwver: v1.00In Derate1 tREFI1x SR93 PD13 R ddrconf 4 rgef0 rgcsb0 1 ERR: Read gate CS0 err error ERR …...
第6节 Node.js 回调函数
Node.js 异步编程的直接体现就是回调。 异步编程依托于回调来实现,但不能说使用了回调后程序就异步化了。 回调函数在完成任务后就会被调用,Node 使用了大量的回调函数,Node 所有 API 都支持回调函数。 例如,我们可以一边读取文…...