Java中的volatile关键字经典应用场景和常见问题

 更新时间:2026年03月03日 08:35:13   作者:身如柳絮随风扬  
这篇文章主要介绍了Java中volatile关键字经典应用场景和常见问题的相关资料,volatile是一种轻量级锁的实现,它针对的仅仅是共享变量,不会对线程加锁,更不会造成线程的阻塞,需要的朋友可以参考下

一、概述

volatile 是 Java 中用于修饰变量的关键字,它提供了一种轻量级的线程间通信机制。与 synchronized 相比,volatile 不会引起线程上下文切换和调度,因此性能开销更小。然而,其同步能力有限,使用不当容易产生线程安全问题。

二、核心特性

1.可见性保证(Visibility)

  • 当某个线程修改 volatile 变量的值时,该值会立即被强制刷新到主内存
  • 其他线程读取该变量时,会从主内存重新加载最新值,而非使用本地线程缓存
  • 解决了多线程环境下因 CPU 缓存导致的数据不一致问题

2.有序性保证(Ordering)

  • 禁止编译器和处理器对 volatile 变量的读写操作进行指令重排序
  • 确保:
    • volatile 写操作之前的任何读写操作不会被重排序到写之后
    • volatile 读操作之后的任何读写操作不会被重排序到读之前
  • 建立 happens-before 关系,确保多线程间的操作顺序可见性

3.不保证原子性(Non-Atomicity)

  • volatile 无法保证复合操作的原子性(如 i++
  • 复合操作由多个步骤组成,中间可能被其他线程中断

三、内存语义

volatile 写操作的内存屏障

[普通写/读操作]  →  [StoreStore屏障]  →  [volatile写]  →  [StoreLoad屏障]

volatile 读操作的内存屏障

[volatile读]  →  [LoadLoad屏障]  →  [LoadStore屏障]  →  [普通写/读操作]

内存屏障的作用:

  1. StoreStore屏障:确保 volatile 写之前的普通写操作已刷新到主内存
  2. StoreLoad屏障:确保 volatile 写完成后,后续的读操作能看到所有之前的写入
  3. LoadLoad屏障:确保 volatile 读之后的操作不会被重排序到读之前
  4. LoadStore屏障:确保 volatile 读之后的写操作不会被重排序到读之前

四、经典应用场景

1.状态标志(最常用)

public class ShutdownHandler {
    private volatile boolean shutdownRequested = false;
    
    public void shutdown() {
        shutdownRequested = true;
    }
    
    public void doWork() {
        while (!shutdownRequested) {
            // 执行任务
        }
    }
}

2.双重检查锁定单例模式(DCL)

public class Singleton {
    // 必须使用 volatile 防止指令重排序
    private static volatile Singleton instance;
    
    private Singleton() {}
    
    public static Singleton getInstance() {
        if (instance == null) {                     // 第一次检查:避免不必要的同步
            synchronized (Singleton.class) {
                if (instance == null) {             // 第二次检查:确保单例
                    // 创建对象分为三步(无volatile可能重排序):
                    // 1. 分配内存空间
                    // 2. 初始化对象
                    // 3. 将引用指向内存地址
                    // volatile 确保 2 在 3 之前完成
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

3.一次性安全发布(One-Time Safe Publication)

public class Resource {
    private volatile Resource resource;
    
    public Resource getResource() {
        if (resource == null) {
            synchronized (this) {
                if (resource == null) {
                    resource = new Resource();
                }
            }
        }
        return resource;
    }
}

4.独立观察(Independent Observation)

public class SensorReader {
    private volatile double currentTemperature;
    
    // 一个线程定期更新温度
    public void updateTemperature(double temp) {
        currentTemperature = temp;
    }
    
    // 多个线程同时读取最新的温度值
    public double getTemperature() {
        return currentTemperature;
    }
}

五、volatile与synchronized详细对比

特性volatilesynchronized
原子性仅保证单个读/写操作的原子性
不保证复合操作(如 i++)的原子性
保证整个代码块/方法的原子性
可见性保证变量对所有线程立即可见保证变量对所有线程可见
有序性禁止指令重排序
(通过内存屏障实现)
保证有序性
(但允许同步块内重排序)
阻塞性非阻塞机制
线程不会挂起
阻塞机制
获取不到锁的线程会挂起等待
性能轻量级,性能开销小
(仅内存屏障开销)
重量级,性能开销较大
(涉及锁竞争、上下文切换)
作用范围变量级别代码块或方法级别
适用场景状态标志、一次性发布复杂同步逻辑、需要原子性的复合操作

六、常见误区与注意事项

错误示例:误以为 volatile 能保证原子性

public class Counter {
    private volatile int count = 0;
    
    // 线程不安全!count++ 不是原子操作
    public void increment() {
        count++;  // 实际包含:读 → 加1 → 写 三个步骤
    }
}

正确解决方案

// 方案1:使用 synchronized(适合复杂同步)
public class SynchronizedCounter {
    private int count = 0;
    
    public synchronized void increment() {
        count++;
    }
}

// 方案2:使用原子类(推荐,性能更好)
import java.util.concurrent.atomic.AtomicInteger;

public class AtomicCounter {
    private AtomicInteger count = new AtomicInteger(0);
    
    public void increment() {
        count.incrementAndGet();  // CAS操作,保证原子性
    }
    
    public int getCount() {
        return count.get();
    }
}

// 方案3:使用 volatile + CAS(高级用法)
public class VolatileCASCounter {
    private volatile int count = 0;
    private static final sun.misc.Unsafe UNSAFE = // 获取Unsafe实例
    
    public void increment() {
        int current;
        do {
            current = count;
        } while (!UNSAFE.compareAndSwapInt(this, OFFSET, current, current + 1));
    }
}

使用建议总结

  1. 优先考虑不可变对象:避免使用 volatile,设计为不可变对象
  2. 一个写线程,多个读线程:典型的 volatile 适用场景
  3. 变量不参与不变性约束volatile 变量不应依赖于其他变量,也不应被其他变量依赖
  4. 替代方案优先:考虑使用 java.util.concurrent.atomic 包下的原子类
  5. 谨慎使用:只在明确理解其语义的场景下使用

七、与 JMM(Java内存模型)的关系

volatile 变量的读写操作建立了 happens-before 关系:

  • volatile 变量的写操作 happens-before 于后续对该变量的读操作
  • 这与 synchronized 的释放锁 happens-before 于获取锁的语义类似

八、性能考量

性能测试对比

// volatile 变量访问 vs 普通变量访问
volatile int vCounter = 0;
int counter = 0;

// volatile 访问有约10-20%的性能损耗
// 但相比 synchronized 的数千倍性能损耗可忽略不计

最佳实践

  1. 避免过度使用:不必要的 volatile 修饰会增加内存屏障开销
  2. 结合使用volatile 适合与 final 结合,用于安全发布
  3. 替代方案:考虑使用 ThreadLocal 避免共享变量

九、扩展知识:volatile 在 JSR-133 中的增强

在 Java 5 之前,volatile 只保证可见性,不保证有序性。JSR-133(Java内存模型修订)强化了 volatile 的语义,通过内存屏障同时保证了可见性和有序性。

十、总结

volatile 是 Java 并发编程中的重要工具,但理解其局限性至关重要:

适合使用 volatile 的场景:

  • 状态标志位(一个线程写,多个线程读)
  • 一次性安全发布(如单例模式的DCL实现)
  • 独立观察(如传感器数据读取)

不适合使用 volatile 的场景:

  • 需要原子性的复合操作
  • 变量参与不变性约束
  • 依赖其他变量的计算

黄金法则: 当您不确定是否需要使用 volatile 时,优先考虑使用更高层次的并发工具(如 java.util.concurrent 包中的类),这些工具通常封装了更安全、更高效的并发实现。

到此这篇关于Java中volatile关键字经典应用场景和常见问题的文章就介绍到这了,更多相关Java中volatile关键字内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • Nacos源码阅读方法

    Nacos源码阅读方法

    这篇文章将会带大家阅读Nacos源码以及教大家阅读源码的技巧,感兴趣的朋友跟随小编一起看看Nacos源码阅读方法
    2022-03-03
  • SpringBoot详细讲解断言机制原理

    SpringBoot详细讲解断言机制原理

    断言Assertion是测试方法中的核心部分,用来对测试需要满足的条件进行验证。这些断言方法都是org.junit.jupiter.api.Assertions的静态方法。检查业务逻辑返回的数据是否合理。所有的测试运行结束以后,会有一个详细的测试报告
    2022-06-06
  • SpringBoot中的@ConfigurationProperties注解解析

    SpringBoot中的@ConfigurationProperties注解解析

    这篇文章主要介绍了SpringBoot中的@ConfigurationProperties注解解析,Spring源码中大量使用了ConfigurationProperties注解,通过与其他注解配合使用,能够实现Bean的按需配置,该注解可以放在类上,也可以放在方法上,需要的朋友可以参考下
    2023-11-11
  • SpringBoot使用Validation实现接口校验的超全使用指南

    SpringBoot使用Validation实现接口校验的超全使用指南

    这篇文章主要为大家详细介绍了SpringBoot使用Validation实现接口校验的相关知识,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下
    2026-03-03
  • Java解析XML文件开源库DOM4J

    Java解析XML文件开源库DOM4J

    dom4j是一个Java的XML API,是jdom的升级品,用来读写XML文件的。dom4j是一个十分优秀的JavaXML API,具有性能优异、功能强大和极其易使用的特点,它的性能超过sun公司官方的dom技术,同时它也是一个开放源代码的软件
    2023-01-01
  • Java中for与foreach的区别

    Java中for与foreach的区别

    本文主要介绍了Java中for与foreach的区别,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2022-05-05
  • SpringBoot如何从配置文件中读取配置参数

    SpringBoot如何从配置文件中读取配置参数

    这篇文章主要介绍了SpringBoot如何从配置文件中读取配置参数问题,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教
    2024-01-01
  • 为什么Java要把字符串设计成不可变的

    为什么Java要把字符串设计成不可变的

    为什么Java要把字符串设计成不可变的,这篇文章给出了Java字符串设计成不可变的原因,感兴趣的小伙伴们可以参考一下
    2017-06-06
  • idea项目的左侧目录没了如何设置

    idea项目的左侧目录没了如何设置

    这篇文章主要介绍了idea项目的左侧目录没了如何设置的操作,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2021-02-02
  • Java中进程与线程的区别

    Java中进程与线程的区别

    这篇文章主要介绍了Java进程与线程的区别,进程(Process)是操作系统分配资源的基本单位,线程(Thread)是操作系统能够进行运算调度的基本单位,下文更多两者区别。需要的小伙伴可以参考一下
    2022-05-05

最新评论