乐途乐途
主页
  • 计算机基础

    • TCP/IP
    • Linux
    • HTTP
  • 数据库

    • SQL
    • MySQL 5.7
  • 编程语言

    • C
    • C++
    • Java SE
    • Python2
    • Python3
  • 数据格式

    • JSON
    • XML
  • 认证与安全

    • JWT
  • 工具

    • Markdown
  • Git

    • GitFlow
  • Quartz

    • Quartz
  • Java

    • Maven 入门
    • Maven 进阶
    • MyBatis
    • Spring
    • Spring MVC
  • Java

    • Spring Boot
    • Spring Cloud
    • Spring Cloud Alibaba
    • Spring Security
    • Spring AI
    • Spring Batch
    • Kafka
    • Java 设计模式
  • 缓存

    • Redis
  • 搜索引擎

    • Elasticsearch
  • 分布式协调

    • ZooKeeper
联系
阿里云
主页
  • 计算机基础

    • TCP/IP
    • Linux
    • HTTP
  • 数据库

    • SQL
    • MySQL 5.7
  • 编程语言

    • C
    • C++
    • Java SE
    • Python2
    • Python3
  • 数据格式

    • JSON
    • XML
  • 认证与安全

    • JWT
  • 工具

    • Markdown
  • Git

    • GitFlow
  • Quartz

    • Quartz
  • Java

    • Maven 入门
    • Maven 进阶
    • MyBatis
    • Spring
    • Spring MVC
  • Java

    • Spring Boot
    • Spring Cloud
    • Spring Cloud Alibaba
    • Spring Security
    • Spring AI
    • Spring Batch
    • Kafka
    • Java 设计模式
  • 缓存

    • Redis
  • 搜索引擎

    • Elasticsearch
  • 分布式协调

    • ZooKeeper
联系
阿里云
  • 学习路径
  • 第1章 Java概述与环境搭建

    • Java概述与环境搭建
    • Java语言概述
    • 解释型语言与编译型语言对比
    • JDK安装与配置
    • JDK、JRE、JVM 详解
    • HelloWorld程序详解
    • IDE 介绍
  • 第2章 标识符与基本数据类型

    • 章节导读
    • 变量概述
    • 常量概述
    • 基本类型与包装类
    • 字节型 byte
    • 短整型 short
    • 整型 int
    • 长整型 long
    • 单精度浮点型 float
    • 双精度浮点型 double
    • 字符型 char
    • 布尔型 boolean
    • 类型转换
  • 第3章 运算符与表达式

    • 章节导读
    • 算术运算符
    • 赋值运算符
    • 关系运算符
    • 逻辑运算符
    • 位运算符
    • 条件运算符
    • 运算符优先级
    • 表达式
  • 第4章 流程控制

    • 章节导读
    • 常见的程序运行流程
    • if-else 选择结构
    • switch 多分支选择
    • while 循环
    • do-while 循环
    • for 循环
    • break 与 continue
  • 第5章 数组

    • 章节导读
    • 一维数组
    • 多维数组
    • Arrays 工具类
  • 第6章 类与对象

    • 章节导读
    • 类与对象
    • 方法定义与调用
    • 构造方法
    • 封装
    • 访问修饰符
    • package 与 import
    • static 关键字
    • this 关键字
    • 参数传递 详解
    • 枚举
    • 成员内部类
    • 局部内部类
    • 静态内部类
    • 匿名内部类
  • 第7章 接口与继承

    • 章节导读
    • 继承
    • super 关键字
    • final 关键字
    • 多态
    • 向上转型与向下转型
    • 抽象类
    • 接口
    • 抽象类与接口对比
  • 第8章 注解

    • 章节导读
    • 注解基础
    • 元注解详解
    • 自定义注解
  • 第9章 常用类

    • 章节导读:Java 常用类
    • Object 类:万类之祖
    • 包装类:基本类型的对象化
    • String:不可变的字符串
    • StringBuffer:线程安全的可变字符串
    • StringBuilder:可变的字符串构建器
    • Math:数学运算工具类
    • Random:伪随机数生成器
    • 大数值运算 详解
    • 日期时间API 详解
  • 第10章 异常机制

    • 章节导读
    • 异常体系与分类
    • try-catch-finally
    • try-with-resources
    • throws 与 throw
    • 自定义异常
  • 第11章 泛型

    • 章节导读
    • 泛型基础
    • 通配符与PECS原则
    • 类型擦除
  • 第12章 集合框架

    • 章节导读
    • 集合框架概述
    • ArrayList
    • LinkedList
    • HashMap 详解
    • LinkedHashMap 详解
    • TreeMap 详解
    • HashSet
    • TreeSet 详解
    • TreeSet 与 Comparable
    • Collections 工具类详解
  • 第13章 IO流

    • 章节导读
    • IO流概述
    • 字节流
    • 字符流
    • 缓冲流
    • 转换流 详解
    • 序列化 详解
    • NIO与Files 详解
    • NIO与Files工具类
  • 第14章 多线程与并发

    • 第十六章 多线程与并发 —— 章节导读
    • 线程基础详解
    • synchronized 详解
    • Lock 与显式锁详解
    • volatile 详解
    • wait 与 notify 详解
    • ThreadLocal详解
    • 原子类详解
    • 并发工具类详解
    • 线程池详解
  • 第15章 反射

    • 章节导读
    • 反射概述与 Class 对象
    • Constructor 与对象创建
    • Field与Method详解
    • 反射应用详解
  • 第16章 JDK8新特性

    • 章节导读
    • Lambda 表达式
    • Stream API 基础
    • Stream API 高级详解
    • Optional 详解
    • 新日期时间API详解
  • 第17章 JDK9-11新特性

    • 章节导读
    • 模块化系统 — Project Jigsaw(JDK 9)
    • var 局部变量类型推断(JDK 10)
    • 集合工厂方法与增强(JDK 9 / 10 / 11)
    • 接口增强:private 方法(JDK 9)
    • Stream API 增强(JDK 9)
    • Optional 增强(JDK 9 / 10 / 11)
    • String 新增方法(JDK 11)
    • HTTP Client 与 Files 增强(JDK 11)
    • 直接运行 Java 源文件 — JEP 330(JDK 11)
  • 第18章 JDK12-17新特性

    • 章节导读
    • Switch 表达式(JDK 12 预览 / JDK 14 正式)
    • 文本块 Text Blocks(JDK 13 预览 / JDK 15 正式)
    • Records 记录类(JDK 14 预览 / JDK 16 正式)
    • 密封类 Sealed Classes(JDK 15 预览 / JDK 17 正式)
    • instanceof 模式匹配(JDK 14 预览 / JDK 16 正式)
    • Switch 模式匹配 — Pattern Matching for switch(JDK 17 预览 / JDK 21 正式)
    • Helpful NPE 与 String 增强(JDK 12 / JDK 14 / JDK 15)
    • Stream 增强(JDK 12 / JDK 16)
    • 日期时间增强 — Day Period 支持(JDK 16)
  • 第19章 JDK18-21新特性

    • 章节导读
    • 虚拟线程(JDK 19 预览 / JDK 20 第二预览 / JDK 21 正式)
    • 序列集合(JDK 21 正式)
    • Switch 模式匹配(JDK 17 预览 / JDK 18 第二预览 / JDK 20 第四预览 / JDK 21 正式)
    • Record 模式匹配(JDK 19 预览 / JDK 20 第二预览 / JDK 21 正式)
    • 未命名模式与变量(JDK 21 预览 / JDK 22 正式)
  • 第20章 JDK 22-25 新特性

    • 章节导读
    • 字符串模板(JDK 22 预览 / JDK 23 第二预览 / JDK 24 第三预览)
    • Stream Gatherers(JDK 22 预览 / JDK 24 第二预览)
    • 隐式声明类与实例方法(JDK 23 预览 / JDK 24 第二预览)
    • 原始类型模式匹配(JDK 24 预览)
  • 附录

    • Java 核心知识点
    • Java SE 专业术语
    • Java特性索引(JDK 8 → 25)

volatile 详解

场景:飞翔科技的监控系统出了一个诡异的问题——大翔把系统的运行状态存在一个 boolean 标志位里,主线程修改了标志,但后台线程死活看不到变化。白歌排查了半天发现:变量没有用 volatile 修饰,CPU 缓存不一致。

白歌(敲着白板):"CPU 有多级缓存。每个核心有自己的 L1/L2 Cache,共享 L3 Cache 和主存。线程 A 修改的变量可能只写到了 L1 Cache,线程 B 读到的是自己 L1 Cache 里的旧值——这就是可见性问题。"

小崔(一脸困惑):"那我用 synchronized 不也能保证可见性吗?volatile 和 synchronized 有啥区别?"

Frank:"volatile is a lightweight synchronization mechanism — it only guarantees visibility, not atomicity. For a simple status flag, volatile is perfect. But for counter++ operations, you still need synchronized or AtomicInteger."

朱璐:"还有经典的 DCL 单例,不用 volatile 的话,指令重排可能导致拿到半成品对象。JDK 5 之后 volatile 增强了 happens-before 语义,才彻底解决了这个问题。"

大翔:"那就从 JMM、缓存一致性、内存屏障,一步步讲清楚。"


一、定义速查表

属性说明
volatileJava 轻量级同步关键字,保证变量的可见性和有序性(禁止指令重排),但不保证原子性。读写 volatile 变量相当于加了一层内存屏障
可见性(Visibility)一个线程对共享变量的修改,其他线程能立即可见。volatile 写立即刷新到主存,volatile 读直接从主存读取
Java 内存模型(JMM)规范了 JVM 中多线程如何通过内存交互的抽象模型。定义了主内存 + 工作内存的抽象,以及 happens-before 规则
happens-beforeJMM 定义的偏序关系,保证操作之间的内存可见性。volatile 写 happens-before 后续 volatile 读
内存屏障(Memory Barrier)CPU 指令,强制将缓存写入主存或使缓存失效。分为 Load Barrier、Store Barrier、Full Barrier(对应 MFENCE/LFENCE/SFENCE 指令)
MESI 协议CPU 缓存一致性协议,保证多核 Cache 中数据一致。四个状态:Modified(已修改)、Exclusive(独占)、Shared(共享)、Invalid(无效)
指令重排(Reordering)编译器和 CPU 为了优化性能,可能调整指令执行顺序。volatile 通过内存屏障禁止特定重排
DCL(Double-Checked Locking)双重检查锁定单例模式,volatile 阻止指令重排导致的"半成品对象"问题
原子性(Atomicity)一个或多个操作作为一个不可分割的整体执行。volatile 不保证复合操作(如 i++ 读-改-写三步)的原子性
Store BufferCPU 写缓存,写操作先写入 Store Buffer 再异步刷新到 Cache。volatile 写会强制刷新 Store Buffer

二、Mermaid 图解

2.1 CPU 缓存写回与 MESI 协议交互

2.2 DCL 双重检查锁定流程图


三、volatile 三大特性

特性说明volatile 是否支持
可见性一个线程修改,其他线程立即可见✅ 支持
有序性禁止指令重排(通过内存屏障)✅ 支持
原子性操作不可分割(如 i++)❌ 不支持

volatile 实现原理:

  • volatile 写:JVM 在写操作后插入 StoreStore 屏障 + StoreLoad 屏障,强制刷新写缓冲到主存
  • volatile 读:JVM 在读操作前插入 LoadLoad 屏障 + LoadStore 屏障,强制从主存读取最新值

四、完整代码示例

示例 1:飞翔科技系统运行状态标志位 —— volatile 可见性验证

场景:飞翔科技的监控平台有 3 个后台工作线程持续运行,管理员(大翔,工号 10001)通过修改 volatile 标志位来优雅停机。

/**
 * 飞翔科技 - 系统运行状态标志位 volatile 可见性演示
 * 
 * 场景:3 个工作线程(数据采集、日志分析、告警检查)
 *       管理员大翔(工号10001)发出停机指令,所有线程立即感知
 */
public class SystemMonitorDemo {

    // ★ volatile — 保证 running 的修改对所有线程可见
    //   如果不加 volatile,工作线程可能永远看不到 main 线程的修改
    private static volatile boolean running = true;

    // 使用 synchronized 保护的非 volatile 计数器(仅用于统计)
    private static int processedCount = 0;
    private static final Object countLock = new Object();

    static class WorkerThread extends Thread {
        private final String workerName;

        public WorkerThread(String name) {
            super(name);
            this.workerName = name;
        }

        @Override
        public void run() {
            int localCount = 0;
            System.out.printf("[%s] 启动,开始监控...\n", workerName);

            // ★ 循环检测 volatile 标志位 — 每次读取都从主内存获取最新值
            while (running) {
                try {
                    Thread.sleep(100); // 模拟每次处理耗时 100ms
                } catch (InterruptedException e) {
                    System.out.printf("[%s] 收到中断信号\n", workerName);
                    break;
                }

                localCount++;
                synchronized (countLock) {
                    processedCount++;
                }

                if (localCount % 10 == 0) {
                    System.out.printf("[%s] 已处理 %d 条数据...\n", workerName, localCount);
                }
            }

            System.out.printf("[%s] 检测到停机标志,正在退出...(共处理 %d 条)\n",
                    workerName, localCount);
        }
    }

    public static void main(String[] args) throws InterruptedException {
        System.out.println("========== 飞翔科技系统监控平台 ==========\n");

        // 启动 3 个工作线程
        WorkerThread w1 = new WorkerThread("数据采集线程");
        WorkerThread w2 = new WorkerThread("日志分析线程");
        WorkerThread w3 = new WorkerThread("告警检查线程");

        w1.start();
        w2.start();
        w3.start();

        // 主线程(管理员大翔-10001)模拟系统运行 5 秒后停机
        System.out.println("[管理员-大翔-10001] 系统正常运行中,薪资结算基数 8888.88...\n");
        Thread.sleep(5000);

        System.out.println("\n[管理员-大翔-10001] 收到维护通知,发起系统停机...");
        // ★ 修改 volatile 变量 — 所有工作线程的 while(running) 立即可见
        running = false;
        System.out.println("[管理员-大翔-10001] 停机标志已设置,等待工作线程退出...\n");

        // 等待工作线程结束
        w1.join(3000);
        w2.join(3000);
        w3.join(3000);

        // 如果线程在 sleep 中,需要 interrupt
        w1.interrupt();
        w2.interrupt();
        w3.interrupt();

        w1.join();
        w2.join();
        w3.join();

        System.out.printf("\n========== 系统已安全停机 ==========\n");
        System.out.printf("总计处理数据: %d 条\n", processedCount);
        System.out.printf("管理员工号: 10001, 月薪参考: %.2f\n", 8888.88f);
    }
}

控制台输出:

========== 飞翔科技系统监控平台 ==========

[管理员-大翔-10001] 系统正常运行中,薪资结算基数 8888.88...

[数据采集线程] 启动,开始监控...
[日志分析线程] 启动,开始监控...
[告警检查线程] 启动,开始监控...
[数据采集线程] 已处理 10 条数据...
[日志分析线程] 已处理 10 条数据...
[告警检查线程] 已处理 10 条数据...
...(5 秒内持续输出)

[管理员-大翔-10001] 收到维护通知,发起系统停机...
[管理员-大翔-10001] 停机标志已设置,等待工作线程退出...

[数据采集线程] 检测到停机标志,正在退出...(共处理 50 条)
[日志分析线程] 检测到停机标志,正在退出...(共处理 49 条)
[告警检查线程] 检测到停机标志,正在退出...(共处理 50 条)

========== 系统已安全停机 ==========
总计处理数据: 149 条
管理员工号: 10001, 月薪参考: 8888.88

示例 2:volatile 计数器证明非原子性 —— i++ 的陷阱

场景:大翔错误地用 volatile 修饰计数器,让 10 个线程各加 10000 次。期望结果 100000,实际远小于此。

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * 飞翔科技 - volatile 非原子性验证
 * 
 * 场景:10 个线程,每个线程执行 10000 次 count++
 *       volatile 修饰的计数器无法得到正确结果
 *       对比:synchronized 和 AtomicInteger 都是正确的
 */
public class VolatileAtomicityDemo {

    // ✗ volatile — 不能保证 count++ 的原子性(读-改-写三步)
    private static volatile int volatileCount = 0;

    // ✓ synchronized — 保证原子性
    private static int syncCount = 0;
    private static final Object lock = new Object();

    // ✓ AtomicInteger — 推荐方式
    private static final AtomicInteger atomicCount = new AtomicInteger(0);

    private static final int THREAD_COUNT = 10;
    private static final int ITERATIONS = 10000;
    private static final int EXPECTED = THREAD_COUNT * ITERATIONS; // 100000

    public static void main(String[] args) throws InterruptedException {
        System.out.println("========== 飞翔科技计数器非原子性验证 ==========");
        System.out.printf("线程数: %d | 每线程操作: %d | 期望: %d\n\n",
                THREAD_COUNT, ITERATIONS, EXPECTED);

        testVolatileCounter();
        testSynchronizedCounter();
        testAtomicCounter();

        System.out.println("\n========== 结论 ==========");
        System.out.println("volatile 只保证可见性,不能保证 count++ 的原子性。");
        System.out.println("复合操作必须使用 synchronized 或 AtomicInteger。");
    }

    private static void testVolatileCounter() throws InterruptedException {
        volatileCount = 0;
        CountDownLatch latch = new CountDownLatch(THREAD_COUNT);
        System.out.println("▶ 测试1: volatile int count = 0; count++");

        for (int i = 0; i < THREAD_COUNT; i++) {
            final int workerId = 10001 + i;
            new Thread(() -> {
                for (int j = 0; j < ITERATIONS; j++) {
                    volatileCount++; // 读-改-写,非原子!
                }
                latch.countDown();
            }, "Worker-" + workerId).start();
        }
        latch.await();
        System.out.printf("  结果: %d (期望: %d, 丢失: %d)\n\n",
                volatileCount, EXPECTED, EXPECTED - volatileCount);
    }

    private static void testSynchronizedCounter() throws InterruptedException {
        syncCount = 0;
        CountDownLatch latch = new CountDownLatch(THREAD_COUNT);
        System.out.println("▶ 测试2: synchronized 保护 count++");

        for (int i = 0; i < THREAD_COUNT; i++) {
            final int workerId = 10001 + i;
            new Thread(() -> {
                for (int j = 0; j < ITERATIONS; j++) {
                    synchronized (lock) { syncCount++; }
                }
                latch.countDown();
            }, "Worker-" + workerId).start();
        }
        latch.await();
        System.out.printf("  结果: %d (期望: %d)\n\n", syncCount, EXPECTED);
    }

    private static void testAtomicCounter() throws InterruptedException {
        atomicCount.set(0);
        CountDownLatch latch = new CountDownLatch(THREAD_COUNT);
        System.out.println("▶ 测试3: AtomicInteger 计数");

        for (int i = 0; i < THREAD_COUNT; i++) {
            final int workerId = 10001 + i;
            new Thread(() -> {
                for (int j = 0; j < ITERATIONS; j++) {
                    atomicCount.incrementAndGet(); // CAS 原子操作
                }
                latch.countDown();
            }, "Worker-" + workerId).start();
        }
        latch.await();
        System.out.printf("  结果: %d (期望: %d)\n\n", atomicCount.get(), EXPECTED);
    }
}

控制台输出:

========== 飞翔科技计数器非原子性验证 ==========
线程数: 10 | 每线程操作: 10000 | 期望: 100000

▶ 测试1: volatile int count = 0; count++
  结果: 23456 (期望: 100000, 丢失: 76544)     ← 丢失大量更新!

▶ 测试2: synchronized 保护 count++
  结果: 100000 (期望: 100000)                  ← 正确!

▶ 测试3: AtomicInteger 计数
  结果: 100000 (期望: 100000)                  ← 正确!

========== 结论 ==========
volatile 只保证可见性,不能保证 count++ 的原子性。
复合操作必须使用 synchronized 或 AtomicInteger。

示例 3:DCL 单例 —— volatile 防止指令重排

场景:飞翔科技的配置管理器使用 DCL 单例模式,volatile 防止"半成品对象"问题。

/**
 * 飞翔科技 - DCL 单例 volatile 防止指令重排
 * 
 * 场景:全局配置管理器,频繁被多线程访问
 *       volatile 确保 INSTANCE 的初始化不会被指令重排破坏
 */
public class DCLSingletonDemo {

    static class ConfigManager {
        // ★ volatile — 禁止指令重排,防止返回半成品对象
        private static volatile ConfigManager INSTANCE;

        private String dbUrl;
        private float maxSalary; // 飞翔科技薪资上限

        private ConfigManager() {
            // 模拟初始化耗时操作
            this.dbUrl = "jdbc:mysql://feixiang-db:3306/fx_tech";
            this.maxSalary = 8888.88f;
            System.out.println("ConfigManager 初始化完成");
        }

        public static ConfigManager getInstance() {
            // 第一次检查 — 避免不必要的同步
            if (INSTANCE == null) {
                // 同步块 — 只有一个线程能进入
                synchronized (ConfigManager.class) {
                    // 第二次检查 — 防止重复创建
                    if (INSTANCE == null) {
                        // ★ 如果没有 volatile,这里可能被指令重排:
                        //   1. 分配内存空间
                        //   2. INSTANCE 指向内存(此时对象还未初始化!)
                        //   3. 调用构造器初始化
                        //   → 线程B在步骤2后读到 INSTANCE != null,拿到半成品!
                        INSTANCE = new ConfigManager();
                    }
                }
            }
            return INSTANCE;
        }

        public float getMaxSalary() { return maxSalary; }
    }

    public static void main(String[] args) {
        System.out.println("========== 飞翔科技配置管理器 DCL 单例测试 ==========\n");

        // 10 个线程同时获取单例
        for (int i = 0; i < 10; i++) {
            final int workerId = 10001 + i;
            new Thread(() -> {
                ConfigManager cm = ConfigManager.getInstance();
                System.out.printf("[工号%d] 获取 ConfigManager, maxSalary=%.2f\n",
                        workerId, cm.getMaxSalary());
            }).start();
        }
    }
}

控制台输出:

========== 飞翔科技配置管理器 DCL 单例测试 ==========

ConfigManager 初始化完成                     ← 只初始化一次!
[工号10001] 获取 ConfigManager, maxSalary=8888.88
[工号10003] 获取 ConfigManager, maxSalary=8888.88
[工号10005] 获取 ConfigManager, maxSalary=8888.88
[工号10002] 获取 ConfigManager, maxSalary=8888.88
[工号10007] 获取 ConfigManager, maxSalary=8888.88
[工号10004] 获取 ConfigManager, maxSalary=8888.88
[工号10006] 获取 ConfigManager, maxSalary=8888.88
[工号10008] 获取 ConfigManager, maxSalary=8888.88
[工号10009] 获取 ConfigManager, maxSalary=8888.88
[工号10010] 获取 ConfigManager, maxSalary=8888.88

五、volatile vs synchronized 对比

对比维度volatilesynchronized
修饰对象只能修饰变量修饰方法/代码块
可见性✅ 保证✅ 保证
原子性❌ 不保证✅ 保证
有序性✅ 禁止指令重排✅ 保证(但内部代码可能重排)
线程阻塞❌ 不阻塞✅ 阻塞未获取锁的线程
性能开销低(内存屏障)较高(锁竞争 + 上下文切换)
适用场景状态标志位、DCL 单例复合操作、临界区保护

六、volatile 的 happens-before 规则

JMM 为 volatile 定义了以下 happens-before 规则:

  1. volatile 写 happens-before 后续 volatile 读:线程 A 写 volatile 变量后,线程 B 读该变量时一定能看到 A 写入的值
  2. volatile 写之前的操作 happens-before volatile 写:A 写 volatile 前对普通变量的修改,在 B 读到 volatile 后也可见
  3. volatile 读之后的操作 happens-after volatile 读:保证后续操作看到的是最新值
线程A:  x = 1;           // 普通写
        flag = true;     // volatile 写 (Store Barrier)

线程B:  if (flag) {      // volatile 读 (Load Barrier)
            // ★ 这里一定能看到 x = 1
            System.out.println(x); // 输出 1
        }

七、易错场景

❌ 错误 1:用 volatile 修饰数组/集合

// ✗ 错误:volatile 修饰引用类型只能保证引用本身的可见性
//         数组元素/集合内容的修改不具有可见性
private volatile int[] data = new int[10];

// 线程A
data[0] = 100;  // ✗ 这个修改对其他线程不一定可见!

// ✓ 正确:使用 AtomicIntegerArray 或 synchronized 保护
// 或使用 volatile 只保证数组引用替换的可见性
data = newData;  // ✓ 引用替换是可见的

❌ 错误 2:认为 volatile 让 i++ 变成原子操作

// ✗ 错误:i++ 是读-改-写三步操作,volatile 无法保证原子性
private volatile int count = 0;
// 多线程执行 count++ → 结果错误

// ✓ 正确:使用 AtomicInteger 或 synchronized
private final AtomicInteger count = new AtomicInteger(0);
count.incrementAndGet();  // ✓ CAS 原子操作

❌ 错误 3:DCL 单例不用 volatile

// ✗ 错误:不加 volatile,INSTANCE = new Singleton() 可能被指令重排
private static Singleton INSTANCE;  // 缺少 volatile!

public static Singleton getInstance() {
    if (INSTANCE == null) {           // 第一次检查
        synchronized (Singleton.class) {
            if (INSTANCE == null) {   // 第二次检查
                INSTANCE = new Singleton(); // ⚠ 可能返回半成品对象!
            }
        }
    }
    return INSTANCE;
}

// ✓ 正确:加上 volatile
private static volatile Singleton INSTANCE;

✅ 正确:volatile 用于状态标志位

// ✓ 正确:volatile 最适合用于简单的布尔标志位
private volatile boolean shutdown = false;

public void run() {
    while (!shutdown) {  // 每次读取都从主内存获取
        doWork();
    }
}

public void stop() {
    shutdown = true;  // 写入立即刷新到主内存
}

八、面试考点

Q1:volatile 的底层实现原理是什么?

A:volatile 通过内存屏障实现。在字节码层面,volatile 变量的读写会触发 JVM 插入内存屏障指令:

  • volatile 写:先插入 StoreStore 屏障(禁止普通写和 volatile 写重排),再插入 StoreLoad 屏障(强制刷新写缓冲到主存,同时让其他 CPU 的缓存失效)
  • volatile 读:先插入 LoadLoad 屏障(禁止 volatile 读和后续普通读重排),再插入 LoadStore 屏障(禁止 volatile 读和后续普通写重排)

在 x86 平台上,volatile 写会生成 lock 前缀指令(如 lock addl),该指令会锁定总线/缓存行,强制写回主存并使其他 CPU 缓存失效。

Q2:volatile 能保证原子性吗?为什么 i++ 不是原子的?

A:不能。i++ 在字节码层面分为三步:getfield(读)→ iconst_1 + iadd(加)→ putfield(写)。volatile 只保证每一步的可见性,但三步之间没有原子性保护。例如:

  • 线程 A 读取 i=0
  • 线程 B 读取 i=0(volatile 读都从主内存取,此时都是 0)
  • 线程 A 计算 0+1=1,写入 i=1
  • 线程 B 计算 0+1=1,写入 i=1
  • 最终 i=1,丢失了一次更新

解决方案:使用 AtomicInteger(CAS 循环)或 synchronized。

Q3:DCL 单例中 volatile 的作用是什么?JDK 5 前后有什么不同?

A:DCL 中 volatile 的作用是禁止指令重排。INSTANCE = new Singleton() 在字节码层面分为三步:

  1. 分配内存空间
  2. 调用构造器初始化对象
  3. 将引用赋值给 INSTANCE

步骤 2 和 3 之间没有数据依赖,可能被重排为 1→3→2。如果线程 A 执行到步骤 3(INSTANCE 已非 null 但对象未初始化),线程 B 第一次检查 INSTANCE == null 为 false,直接返回未初始化的对象,导致错误。

JDK 5 之前 volatile 的 happens-before 语义不完整,不能禁止这种重排。JDK 5(JSR-133)增强了 volatile 语义,才彻底解决了 DCL 问题。

Q4:volatile 和 synchronized 各适用于什么场景?

A:

场景推荐方案
状态标志位(boolean shutdown)volatile
单次赋值(初始化后只读)volatile
DCL 单例中的 INSTANCEvolatile
复合操作(i++、check-then-act)synchronized / AtomicXxx
多个变量的一致性保护synchronized
需要 wait/notify 通信synchronized

大翔(满意地点头):"volatile 是个好东西——轻量、高效,但必须记住它的边界:只保证可见性,不保证原子性。"

白歌:"没错。volatile 像红绿灯,告诉所有线程'这是最新值';synchronized 像安检门,一次只放一个人进去。"

Frank:"And for complex operations, Lock gives you more flexibility — tryLock, interrupt, fair lock. Let's cover that next."

朱璐:"记住:volatile 是 JMM 的基石,理解了 volatile,就理解了 Java 内存模型的一半。"

上一页
Lock 与显式锁详解
下一页
wait 与 notify 详解