JUC 并发编程:对可见性、有序性与 volatile的理解

一、核心概念:可见性、有序性

在 Java 多线程编程中,可见性有序性 是并发问题的核心痛点,也是 Java 内存模型(JMM)需要解决的核心问题,JUC 并发包的诸多特性均围绕这两个问题设计,而volatile是 JMM 提供的轻量级解决手段。

1. 可见性

指一个线程对共享变量的修改,能够及时被其他线程感知到。

  • 问题根源:JMM 中每个线程有自己的工作内存,共享变量存储在主内存,线程操作共享变量时会先拷贝到工作内存,修改后再刷回主内存,若未及时刷回,其他线程读取的仍是旧值,导致脏读
  • 无保障场景:普通共享变量的写操作,JMM 不保证其对其他线程的读操作可见。

2. 有序性

指程序执行的顺序按照代码的书写顺序执行,避免指令重排序

  • 问题根源:JVM 和 CPU 为了优化执行效率,会在不影响单线程执行结果的前提下,对指令进行重排序(编译器重排序、CPU 重排序),但多线程环境下,重排序会导致执行结果异常。
  • 关键原则:happens-before规则是 JMM 定义的有序性和可见性的核心保障,满足该规则的操作,前序操作的结果对后续操作可见,且执行顺序有序。

二、volatile 基础使用

1. volatile 定义

volatile是 Java 的关键字,用于修饰共享变量,是 JMM 提供的轻量级同步机制 ,不保证原子性,但能同时保证可见性有序性 ,底层通过内存屏障禁止指令重排序实现。

2. volatile 核心特性

(1)保证可见性

volatile变量的写操作,会立即将工作内存中的修改刷回主内存 ;对volatile变量的读操作,会直接从主内存读取最新值,清空工作内存的旧值,避免脏读。

(2)保证有序性

禁止 JVM 和 CPU 对volatile变量相关的指令进行重排序,通过在volatile写 / 读操作前后插入内存屏障,固定指令执行顺序。

(3)不保证原子性

volatile无法解决多线程同时写操作的竞态问题,例如i++(读 - 改 - 写)操作,多线程执行时仍会出现数据不一致,需配合锁(synchronized/Lock)或原子类(AtomicInteger)使用。

3. volatile 基本语法

java 复制代码
// 修饰成员变量
volatile static int num = 0;
// 修饰对象引用
volatile static User user = null;
// 修饰实例变量
class Demo {
    volatile int flag = false;
}

4. 基础使用场景:状态标记位

这是volatile最典型的应用场景,用于多线程间的状态通知,保证状态的即时可见。

java 复制代码
public class VolatileBasicDemo {
    // volatile修饰状态标记,保证多线程间可见
    private static volatile boolean isRunning = true;

    public static void main(String[] args) throws InterruptedException {
        // 线程1:执行业务逻辑,根据标记位停止
        new Thread(() -> {
            int i = 0;
            while (isRunning) {
                i++;
            }
            System.out.println("线程1停止,累计执行:" + i + "次");
        }, "t1").start();

        // 主线程休眠1秒,让线程1先执行
        Thread.sleep(1000);
        // 修改标记位,线程1能立即感知并停止
        isRunning = false;
        System.out.println("主线程修改标记位:isRunning = false");
    }
}

结果 :主线程修改isRunning后,线程 1 会立即退出循环,体现volatile的可见性。

三、volatile 与 happens-before 规则

volatile的可见性和有序性,通过happens-before 中的volatile 变量规则传递性规则保障,是其底层核心逻辑。

1. volatile 变量规则

对一个volatile变量的写操作 ,happens-before 于后续对该变量的读操作

  • 核心:volatile写的结果,对所有后续的volatile读可见,且写操作执行顺序早于读操作。

2. 传递性规则结合使用

若操作 A happens-before 操作 B,操作 B happens-before 操作 C,则 A happens-before 操作 C。

  • volatile常与程序顺序规则结合,通过传递性实现普通变量的可见性,示例:
java 复制代码
public class VolatileHappensBeforeDemo {
    private static volatile boolean flag = false;
    private static int num = 0; // 普通变量

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            num = 100;          // 操作1:普通写(程序顺序规则:1 happens-before 2)
            flag = true;         // 操作2:volatile写(volatile规则:2 happens-before 3)
        }, "t1");

        Thread t2 = new Thread(() -> {
            if (flag) {          // 操作3:volatile读(程序顺序规则:3 happens-before 4)
                System.out.println(num); // 操作4:普通读,一定输出100
            }
        }, "t2");

        t1.start();
        t2.start();
        t1.join();
        t2.join();
    }
}

解释 :通过传递性,num=100(操作 1)happens-before System.out.println(num)(操作 4),实现了普通变量的跨线程可见性。

四、volatile 进阶使用

1. 进阶场景 1:双重检查锁定(DCL)实现单例模式

单例模式的懒汉式实现中,多线程环境下会出现指令重排序 导致的对象初始化不完整问题,volatile可解决该问题,是 DCL 单例的核心保障。

(1)无 volatile 的 DCL 问题
java 复制代码
// 有问题的懒汉式单例(多线程下可能获取到未初始化完成的对象)
class Singleton {
    private static Singleton instance = null; // 未加volatile
    private Singleton() {}

    public static Singleton getInstance() {
        if (instance == null) { // 第一次检查
            synchronized (Singleton.class) { // 加锁
                if (instance == null) { // 第二次检查
                    instance = new Singleton(); // 存在指令重排序
                }
            }
        }
        return instance;
    }
}

问题根源instance = new Singleton()分为 3 步,JVM 会重排序:

  1. 分配对象内存空间;2. 初始化对象;3. 将 instance 指向分配的内存地址。重排序后可能出现1→3→2 ,此时其他线程第一次检查会发现instance != null,直接返回未初始化的对象。
(2)加 volatile 的正确 DCL 单例
java 复制代码
// 正确的DCL单例模式(volatile禁止指令重排序)
class Singleton {
    // volatile修饰实例引用,禁止指令重排序
    private static volatile Singleton instance = null;
    private Singleton() {}

    public static Singleton getInstance() {
        if (instance == null) { // 第一次检查,无锁,提高效率
            synchronized (Singleton.class) { // 类锁,保证单例
                if (instance == null) { // 第二次检查,防止多线程重复创建
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

核心volatile禁止instance = new Singleton()的指令重排序,保证步骤1→2→3 执行,确保其他线程看到的instance一定是初始化完成的对象。

2. 进阶场景 2:配合原子类实现高效并发

volatile不保证原子性,但若与JUC 原子类Atomic*)结合,可实现高效的原子操作 + 可见性,替代重量级锁。

java 复制代码
import java.util.concurrent.atomic.AtomicInteger;

public class VolatileAtomicDemo {
    // 原子类保证原子性,volatile保证可见性(原子类底层本身通过volatile修饰变量)
    private static volatile AtomicInteger count = new AtomicInteger(0);

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                count.incrementAndGet(); // 原子自增,替代i++
            }
        }, "t1");

        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                count.incrementAndGet();
            }
        }, "t2");

        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println("最终计数:" + count.get()); // 一定输出2000
    }
}

说明 :JUC 原子类(如AtomicInteger)的底层变量通过volatile修饰,结合 CAS 算法实现原子操作,同时保证可见性。

3. 进阶场景 3:解决普通变量的跨线程可见性

通过volatile的传递性规则,实现普通共享变量的跨线程可见,替代synchronized(轻量级,效率更高),示例参考二、4VolatileHappensBeforeDemo

五、volatile 与 synchronized 对比

volatilesynchronized都是解决并发可见性和有序性的手段,但特性和使用场景差异显著,volatile 是轻量级同步,synchronized 是重量级同步,对比如下:

特性 volatile synchronized
修饰范围 仅能修饰成员变量 / 实例变量 可修饰方法、代码块、静态方法
可见性 保证 保证
有序性 保证 保证(通过监视器锁规则)
原子性 不保证 保证(独占锁,串行执行)
性能 轻量级,无锁,开销极小 重量级,有锁竞争,开销大
阻塞性 非阻塞式同步 阻塞式同步(锁等待)

核心使用原则

  • 若仅需状态标记单例 DCL普通变量的可见性 ,使用volatile,效率更高;
  • 若需原子操作 (如i++)、多步操作的同步 ,使用synchronized或 JUC 的Lock、原子类;
  • volatile可作为synchronized的补充,而非替代。

六、学习总结

1. 核心总结

  • volatile的核心价值:轻量级保证可见性和有序性,是 JMM 中最常用的同步手段之一;
  • volatile的底层实现:内存屏障 (禁止重排序)+主内存直读直写(保证可见性);
  • volatile的局限性:不保证原子性,无法解决多线程写操作的竞态问题;
  • 关键关联:volatile的特性通过happens-beforevolatile 变量规则传递性规则保障,是 JUC 并发编程的基础。

2. 实用使用建议

  1. 优先使用volatile作为状态标记位,这是其最安全、最典型的场景;
  2. 实现DCL 单例模式 时,必须给实例引用加volatile,禁止指令重排序;
  3. 不要用volatile修饰需要多线程原子写的变量,需配合原子类或锁;
  4. 利用volatile的传递性,可实现普通变量的跨线程可见,替代重量级锁;
  5. JUC 原子类底层已使用volatile,无需额外修饰,直接使用即可;
  6. 区分可见性 / 有序性和原子性:若问题是读不到最新值 /指令重排序 ,用volatile;若问题是多线程写冲突,用锁 / 原子类。
相关推荐
无名-CODING2 小时前
Tomcat 底层核心知识点字典(面试必备)
java·面试·tomcat
csbysj20202 小时前
Django ORM - 单表实例
开发语言
XiYang-DING2 小时前
【Java SE】双亲委派模型
java·开发语言
阿阿阿阿里郎2 小时前
ROS2快速入门--C++基础
开发语言·c++·算法
free-elcmacom2 小时前
C++<x>new和delete
开发语言·c++·算法
我命由我123452 小时前
Git 创建新分支并推送到远程仓库
java·服务器·git·后端·学习·java-ee·学习方法
程序喵大人2 小时前
map的[]运算符,这个看似方便的语法,藏着怎样的魔鬼?
开发语言·c++·map·运算符
全栈开发圈2 小时前
新书速览|R语言医学数据分析与可视化
开发语言·数据分析·r语言
014-code2 小时前
手把手带你解读 Dockerfile - 最快上手方法
java·docker·容器·持续部署