volatile关键字的作用是什么?它能保证原子性吗?

volatile 是 Java 中一个轻量级的同步关键字,主要用于修饰变量。它的核心作用是保证可见性禁止指令重排序 ,但不能保证原子性

一、volatile 的两大作用

1. 保证可见性

  • 问题背景:在 Java 内存模型(JMM)中,每个线程有自己的工作内存(缓存),主内存中的变量可能被线程缓存到本地。当一个线程修改了共享变量后,其他线程无法立刻看到这个修改,导致数据不一致。
  • volatile 解决方案
    volatile 修饰的变量,写操作 会立即刷新到主内存;读操作 会直接从主内存读取(跳过本地缓存)。
    这样,一个线程修改了 volatile 变量,其他线程能马上看到最新值。
java 复制代码
// 示例:volatile 保证可见性,用于线程停止标志
class Runner {
    private volatile boolean running = true;  // 不加 volatile,子线程可能永远看不到修改

    public void stop() { running = false; }

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

2. 禁止指令重排序(保证有序性)

  • 问题背景:JVM 和 CPU 为了优化性能,可能会对指令进行重排序(在不改变单线程执行结果的前提下)。在多线程环境下,重排序可能导致意想不到的错误。
  • volatile 解决方案
    volatile 变量的读写操作前后插入内存屏障(Memory Barrier),禁止编译器 / CPU 对相关指令重新排序。
    典型应用:双重检查锁定(Double-Checked Locking)单例模式
java 复制代码
// 错误的单例(不加 volatile,可能返回未完全初始化的对象)
public class Singleton {
    private static volatile Singleton instance;  // 必须 volatile
    public static Singleton getInstance() {
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton(); // 实际分三步:1.分配内存 2.初始化 3.引用指向内存
                }   // volatile 禁止第 2 和 3 步的重排序,防止其他线程看到空引用但未初始化的对象
            }
        }
        return instance;
    }
}

二、volatile 不能保证原子性

为什么不能保证原子性?

原子性是指一个或多个操作要么全部执行且不被中断,要么全不执行。
volatile 只能保证单个读 / 写操作 的原子性(例如读一个 long 或写一个 long 是原子的),但复合操作 (如 i++)不是原子的:
i++ 包含"读 - 改 - 写"三步,在多线程下即使 ivolatile,也会发生丢失更新的问题。

java 复制代码
volatile int count = 0;

// 两个线程各执行 10000 次 count++,最终结果可能小于 20000
void increment() {
    count++;  // 1.读 count 到寄存器 2.加1 3.写回内存 ------ 不是原子操作
}

如何实现原子操作?

  • 使用 synchronizedReentrantLock 对整个复合操作加锁。
  • 使用 java.util.concurrent.atomic 包下的原子类,如 AtomicInteger(基于 CAS 实现,保证原子性)。
java 复制代码
AtomicInteger atomicCount = new AtomicInteger(0);
atomicCount.incrementAndGet();  // 原子自增

三、volatilesynchronized 对比

特性 volatile synchronized
作用 保证可见性、有序性(禁止重排序) 保证原子性、可见性、有序性(通过锁的互斥)
是否阻塞 不阻塞线程 阻塞其他竞争锁的线程
适用对象 只能修饰变量 修饰方法、代码块
原子性保证 不能保证复合操作的原子性 保证代码块内的操作原子执行
性能 比加锁轻量,读写开销小 较重(但现代 JVM 已优化)

四、典型使用场景

1. 状态标志(开关)

java 复制代码
volatile boolean shutdownRequested = false;
// 线程1: shutdownRequested = true;
// 线程2: while (!shutdownRequested) { ... }

2. 双重检查锁定的单例模式(如前述)

3. 读操作远多于写操作的"一写多读"变量

例如统计配置参数、温度传感器数值等,只有一个线程更新,多个线程读取,用 volatile 即可保证可见性,无需加锁。

总结

  • volatile 的作用 :保证变量在多线程间的可见性 以及禁止指令重排序
  • 不能保证原子性 :对于 ++-- 或其他复合操作,仍需加锁或使用原子类。
  • 选择建议 :如果只是需要某个变量的修改对其他线程立即可见,且操作本身是原子的(例如赋值 a = 1,或者 boolean 标志位),用 volatile;如果需要复合操作的原子性,用 synchronizedLock
相关推荐
假如让我当三天老蒯1 小时前
前端跨域解决方案(学习用)
前端·javascript·面试
Colin草率地做慢慢地改1 小时前
关于QuickStore这个项目的重构(2)- 数据库建表文件
后端·面试·架构
JieE21211 小时前
LeetCode 56. 合并区间|超清晰 JS 图解思路,面试高频区间题
javascript·算法·面试
JustHappy20 小时前
我汇总了身边朋友的经历才发现,其实第一份实习是最难找的......
前端·后端·面试
uhakadotcom20 小时前
在python 的 工程化架构中 ,什么是 薄包装器层?
后端·面试·github
假如让我当三天老蒯1 天前
模块化:ES Module 与 CommonJS 的区别
前端·面试
沉默王二1 天前
面试官:RAG 不用向量数据库,用 MySQL 硬扛?我:100 万向量不是很轻松?
mysql·面试·ai编程
逐光老顽童1 天前
Java 与 Kotlin 混合开发避坑指南:30 个真实案例实录
android·kotlin
爱勇宝2 天前
鸿蒙生态的下半场:开发者不只要能开发,还要能赚钱
android·前端·程序员