臭名昭著aTrust隔离计划1.臭名昭著aTrust隔离计划2.VPN原理与aTrust隔离网络实践3.docker-easyconnect到底做了什么4.TUN(tunnel-隧道-虚拟网卡)模式
个人项目
personal(个人展示)GitHub Actions 自动部署个人展示项目
基于Quartz搭建的个人博客1.Quartz个人博客使用教程2.使用 rsync 增量部署 Quartz 博客3.使用 GitHub Actions 自动部署 Quartz 博客5.域名绑定6.CDN加速7.Google 搜索收录排查:为什么关键词搜得到别人,却搜不到我的网站?Obsidian、双向链接与知识图谱
架构设计
分布式分布式事务RPC原理
旅行
行程攻略青岛三日游行程攻略
前端
NginxNginx配置与反向代理入门
Nodenpm与npx的区别
数据库
mysql01.MySQL基础语法与约束02.MySQL数据库类型03.MySQL多表查询04.MySQL 索引05.MySQL 事务06.MySQL 隔离级别07.MySQL MVCC08.MySQL 锁09.EXPLAIN10.慢查询与 SQL 优化
mysql面试题01.索引问题02.事务与隔离级别03.锁机制04.存储引擎与性能优化05.用户权限、字符集与备份恢复
mysql实战01.EXPLAIN实战02.测试环境菜单误删排查与恢复03.MySQL IN 详解
网络
网络安全1.无线监听网卡苹果M系列虚拟机安装Kali Linux教程3.kali怎么使用RTL8812AU4.kali基础命令
网络基础进程之间怎样通过网络传递数据
网址常用网址
中间键
esElasticsearch 入门
mq
RabbitMQRabbitMQ高级RabbitMQ基础
1. MQ 基础2. MQ 核心角色与代码对应3. MQ Consumer Group 与消费模式4. SOFAMQ 账户变更代码实践5. MQ 可靠性问题6. 消息幂等
redis缓存击穿、穿透与雪崩Redis 分布式锁Redis 缓存问题Redis 数据结构
AI
使用说明
第三方插件&技能简介Archify使用与安装指南geekgeekRun使用OfferFlow使用指南Ponytail使用指南Skills Manager使用指南
CodexCodex CLI与IDE区别及使用指南Codex Hook单独配置与提交通知Codex MCP安装与使用指南Codex第三方插件安装与使用指南
Agent开发0-学习路线1.LLM、Token、上下文窗口与模型参数2.大模型 API、请求参数与响应结构3.Spring AI ChatClient4.Prompt、System Prompt、Prompt 模板6.结构化输出、JSON Schema7.SSE 流式响应8.会话 ID、聊天记录、Redis9.超时、重试、限流、降级10.*完成可运行聊天接口11.Function Calling、Tool Calling 原理12.用 Java 方法定义 Tool13.工具参数设计和校验14.实现订单查询工具15.实现物流查询工具16.实现库存查询工具17.多个工具自动选择18.工具异常、超时、重试
diy主机
主机零件学习1.CPU2.主板3.内存4.显卡5.硬盘6.散热7.风扇8.机箱热门型号价格表
捡垃圾diy-13600kf+5060diy-14600kf+5070
English高频词汇英文基础语法简单句子音标
GitGit常用命令Sublime Merge 使用指南
Java
工具库Caffeine本地缓存Reactor教程
工作实践如何使用@Async线程池使用与排查线上服务 CPU 使用率过高排查压测下RPC接口大量失败排查记录
基础1.HashMap2.ConcurrentHashMap3.HashSet4.final5.static6.volatile7.AtomicInteger8.String动态代理反射锁与线程同步线程注解
面试题1.HashMap 存储在方法内是线程安全的吗2.ArrayList参数传递与引用重新赋值3.== 和 equals 的区别、hashcode4.重载和重写的区别5.String、StringBuffer、StringBuilder区别及使用场景6.接口和抽象类7.ArrayList 与 LinkedList 的区别和扩容机制8.HashMap 和 Hashtable 的区别及底层实现9.Object 和 Objects 的区别10.ThreadLocal的原理和使用场景11.字节码是什么及作用12.Java类加载器有哪些13.双亲委派模型14.Java异常体系15.GC是什么16.GC如何判断对象可以被回收
面试总结0.HR面试模板1.面试一2.面试二3.面试三笔试
网络与 IO(黑马程序员-纯理论)01-网络编程基础02-IO模型与零拷贝03-BIO04-NIO基础与缓冲区05-NIO通道与选择器06-AIO
juc00-目录01-进程与线程02-线程同步与通信03-并发设计模式04-Java内存模型05-CAS与原子类06-ThreadLocal07-阻塞队列08-线程池使用与原理09-任务调度10-ForkJoin与享元模式11-AQS12-ReentrantLock13-读写锁14-并发协作工具15-ConcurrentHashMap16-CopyOnWrite与同步容器17-SkipListMap18-非阻塞队列
juc(工作常见)Java并发编程三个重点拆解
jvm1. JVM运行时数据区
Spring1.Spring IoC、DI与构造方法注入CGLIB、Byte Buddy 与 Spring AOPSpring 常用注解详解Spring Bean 生命周期
Spring MVC异常处理机制
linux常用命令
mac快捷键终端常用操作
python
基础01.Python是什么,为什么要安装Python02.Python项目中.venv是什么03.Python基础语法
基础实战sql替换
HTTPXHRMS员工详情接口调用(Python HTTPX)
windowswindows激活

一、💡 一句话理解

核心结论

Java 的 volatile 用来修饰会被多个线程共享的变量。它主要保证可见性和有序性,但不提供互斥,也不能把 count++ 这样的复合操作变成原子操作。

可以先记住一句话:

保证看得见,约束执行顺序,但不负责让线程排队。

二、🧭 理论:它是什么

2.1 从“通知后台线程停止”说起

假设程序里有一个工作线程不断处理任务,主线程负责通知它停止:

private static boolean running = true;

工作线程反复读取 running,主线程在需要停止时执行:

running = false;

如果没有正确的线程同步,Java 内存模型不保证工作线程能够按照预期观察到这次修改。把变量声明为 volatile:

private static volatile boolean running = true;

主线程写入 false 后,工作线程后续读取这个变量时,就能按照 Java 内存模型的规则观察到该写入。

2.2 场景与真实技术对象的对应关系

场景角色真实技术对象
后台工作人员工作线程
管理员主线程
停止通知牌volatile boolean running
管理员更换通知牌对 volatile 变量进行写操作
工作人员查看通知牌对 volatile 变量进行读操作

类比边界

“通知牌”只用于帮助理解线程之间的可见性。volatile 并不会让多个线程排队,也不能保护由多步操作组成的业务流程。

2.3 volatile 适合什么变量

volatile 适合表示简单、独立的共享状态,例如:

  • 是否继续运行;
  • 配置是否已经加载;
  • 当前配置对象的引用;
  • 某项只需要整体替换的状态快照。

如果变量的新值依赖旧值,或者多个字段必须一起保持一致,就不能只依赖 volatile。

三、⚙️ 理论:它是怎么工作的

3.1 可见性:其他线程能够观察到写入

对一个 volatile 变量进行写入后,其他线程后续读取同一个变量时,能够按照 Java 内存模型规定的同步顺序观察到该写入。

主线程:running = false
                  ↓
工作线程:读取 running,观察到 false

这解决的是“一个线程已经改了,另一个线程能不能正确看见”的问题。

不要只背“强制读写主内存”

这种说法适合粗略记忆,但不够准确。正式理解应以 Java 内存模型的 happens-before 和 synchronizes-with 规则为准,而不是把 JVM 简化成“线程缓存和主内存之间每次强制刷新”。

3.2 有序性:建立 happens-before 关系

Java 语言规范规定:

对某个 volatile 字段的写入,happens-before 后续对同一个字段的读取。

happens-before 可以先理解为:前面的操作结果必须对后面的操作可见,并且满足 Java 内存模型规定的先后约束。

例如:

private static int data;
private static volatile boolean ready;
 
// 线程 A
data = 100;
ready = true;
 
// 线程 B
if (ready) {
    System.out.println(data);
}

当线程 B 读取到 ready == true 时,它也能够观察到线程 A 在写入 ready 之前对 data 的写入。这种用法通常称为安全发布状态。

3.3 单次读写具有原子性

Oracle Java 并发教程指出,对 volatile 变量的单次读取和单次写入是原子操作,包括 long 和 double。

例如下面的单次赋值是原子的:

private static volatile long version;
 
version = 10L;

但是,单次读写原子不等于所有使用这个变量的表达式都原子。

3.4 为什么 volatile 不能保证 count++ 安全

private static volatile int count = 0;
 
count++;

count++ 实际包含三个步骤:

读取 count → 计算 count + 1 → 把新值写回 count

假设初始值是 0:

时刻线程 A线程 B最终变量
1读取到 0读取到 00
2计算得到 1计算得到 10
3写入 1写入 11

两个线程明明都执行了一次加一,结果却只是 1。volatile 保证它们能看到共享值,但没有把三步操作合成一个不可分割的整体。

这种并发计数应该使用 7.AtomicInteger:

private static final AtomicInteger COUNT = new AtomicInteger(0);
 
// incrementAndGet:原子地加一,并返回加一后的值。
int newValue = COUNT.incrementAndGet();

3.5 volatile、原子类和锁的区别

工具可见性单变量原子更新多步业务互斥是否可能阻塞
volatile可以仅单次读写不可以通常不会
AtomicInteger 等原子类可以可以一般不可以通常不会
synchronized / Lock可以可以可以可能会

当多个字段必须一起保持一致,或者存在“先判断再修改”的组合逻辑时,通常应使用 锁与线程同步,而不是只添加 volatile。

四、🚀 实践:从准备到验证

4.1 前置准备

准备 JDK 8 或更高版本即可,不需要第三方依赖。

检查 Java 环境:

java -version
javac -version

4.2 可以拿来干什么

4.2.1 通知工作线程停止

用途:一个线程负责执行后台任务,另一个线程只负责修改停止状态。

前置条件:

  • 状态只有 true 和 false;
  • 工作线程只读取状态;
  • 主线程只需要发布停止通知;
  • 不需要把多个业务字段作为一个整体修改。

核心代码:

// volatile:保证停止信号能够被其他线程正确观察。
private static volatile boolean running = true;
 
while (running) {
    // 执行后台任务
}

输入是主线程写入的 false,处理过程是工作线程反复读取 running,结果是工作线程退出循环。

4.2.2 发布已经准备好的数据

用途:线程 A 先准备数据,最后修改 volatile 状态;线程 B 看到状态后再使用数据。

private static String config;
private static volatile boolean ready;
 
static void loadConfig() {
    // 先准备普通数据。
    config = "production";
 
    // 最后发布 ready,建立 volatile 写入语义。
    ready = true;
}
 
static void useConfig() {
    // 读取 ready;只有观察到 true 后才使用已经发布的数据。
    if (ready) {
        System.out.println(config);
    }
}

这里真正承担发布边界的是对 ready 的写入和读取。不要在写入 ready = true 后继续修改本次需要发布的数据。

4.3 完整实践:代码、启动和验证

创建文件 VolatileDemo.java:

public class VolatileDemo {
 
    /*
     * volatile:主线程修改 running 后,
     * 工作线程能够按照 Java 内存模型的规则观察到新值。
     */
    private static volatile boolean running = true;
 
    public static void main(String[] args) throws InterruptedException {
        Thread worker = new Thread(() -> {
            System.out.println("工作线程开始运行");
 
            /*
             * 每次循环都会读取 volatile 变量 running。
             * 当主线程把它修改为 false 后,循环结束。
             */
            while (running) {
                // 模拟持续运行的后台任务。
            }
 
            System.out.println("工作线程收到停止信号");
        }, "worker");
 
        // start:启动工作线程,让它执行上面的任务。
        worker.start();
 
        // sleep:让工作线程先运行一秒,再发送停止通知。
        Thread.sleep(1000);
 
        System.out.println("主线程发送停止信号");
 
        // 写入 volatile 变量,向工作线程发布停止状态。
        running = false;
 
        /*
         * join:等待工作线程真正结束。
         * 工作线程结束前,主线程不会继续打印“程序结束”。
         */
        worker.join();
 
        System.out.println("程序结束");
    }
}

编译:

javac VolatileDemo.java

运行:

java VolatileDemo

预期结果:

工作线程开始运行
主线程发送停止信号
工作线程收到停止信号
程序结束

去掉 volatile 后不一定每次都能复现错误

某次测试仍然正常退出,不代表代码已经线程安全。线程调度、JIT 编译和运行环境都会影响现象;正确性应依据 Java 内存模型的保证,而不能依赖“我运行几次都没出问题”。

4.3.1 常见问题排查

javac: command not found

说明没有安装 JDK,或者 JDK 的 bin 目录没有加入环境变量。先执行 java -version 和 javac -version 检查。

程序一直占用较高 CPU

示例为了突出 volatile,使用了空循环不断读取变量,因此会持续占用 CPU。真实项目通常会使用阻塞队列、中断、等待通知或线程池关闭机制,而不是长期空转。

把 running 改成普通 boolean,程序仍然退出

这是允许出现的现象,但不构成正确性证明。没有 volatile 或其他同步机制时,Java 内存模型不保证程序始终按这个结果运行。

五、🎤 面试时怎么流利地说

5.1 回答路线

volatile 是什么
    ↓
解决可见性和有序性问题
    ↓
volatile 写 happens-before 后续读取
    ↓
不提供互斥,不保证复合操作原子性
    ↓
状态标记用 volatile,计数用原子类,组合状态用锁

5.2 30 秒标准回答

volatile 是 Java 提供的一种轻量级线程同步机制,主要保证共享变量的可见性,并通过 happens-before 规则提供有序性。一个线程写入 volatile 变量后,后续读取同一个变量的线程能够观察到该写入以及写入前的相关操作。但是 volatile 不提供互斥,也不能保证 count++ 这种读、改、写复合操作的原子性。因此状态开关可以使用 volatile,并发计数应使用 AtomicInteger,多个变量需要保持一致时通常使用锁。

5.3 1—2 分钟展开回答

多个线程访问普通共享变量时,可能出现可见性和指令重排序问题。volatile 会让对该变量的读写成为 Java 内存模型中的同步动作。规范规定,对某个 volatile 字段的写入 happens-before 后续对同一个字段的读取,因此读取线程不仅能观察到这个字段的写入,也能观察到写入线程在此之前完成的相关操作。

不过,volatile 只适合独立状态的读写,不会像锁一样让线程互斥。例如 count++ 包含读取、加一和写回三个步骤;即使 count 是 volatile,多个线程仍可能读取相同旧值并覆盖更新。此时应使用 AtomicInteger.incrementAndGet()。如果一次业务操作要同时检查和修改多个变量,则一般使用 synchronized、Lock 或更高层并发工具。

5.4 高频追问与短答

5.4.1 volatile 能保证原子性吗

它只能保证变量单次读取和单次写入的原子性,不能保证 count++、检查后更新等复合操作的原子性。复合更新通常使用原子类或锁。

5.4.2 volatile 和 synchronized 有什么区别

volatile 主要提供可见性和有序性,不提供互斥,线程通常不会因为它而阻塞。synchronized 同时提供互斥和内存可见性,适合保护临界区以及多个状态组成的不变量。

5.4.3 为什么不建议只说“volatile 强制读写主内存”

因为这是简化后的硬件类比,容易误导人把 Java 内存模型理解成固定的缓存刷新流程。更准确的回答是:volatile 读写是同步动作,并通过 synchronizes-with 和 happens-before 规则提供可见性与顺序保证。

六、📌 总结

  • volatile 主要保证共享变量的可见性和有序性。
  • 对一个 volatile 字段的写入 happens-before 后续对同一字段的读取。
  • 它不提供互斥,也不能让 count++ 变成原子操作。
  • 简单状态开关可以用 volatile,单变量并发计数使用原子类,组合状态通常使用锁。
  • 判断并发代码是否正确,应依据 Java 内存模型,而不是依赖偶然的测试结果。

记忆句:一写多读状态用 volatile,并发修改计数用原子类,组合状态用锁。

七、📚 官方资料