【Java并发】【volatile】适合初学者体质的volatile

简介: 当你阅读dalao的框架源码的时候,你是否会见到这样一个关键字 - - - volatie,诶,你是否会好奇,为什么要加它?加了它有什么作用?

👋hi,我不是一名外包公司的员工,也不会偷吃茶水间的零食,我的梦想是能写高端CRUD
🔥 2025本人正在沉淀中... 博客更新速度++
👍 欢迎点赞、收藏、关注,跟上我的更新节奏
📚欢迎订阅专栏,专栏别名《在2B工作中寻求并发是否搞错了什么》

前言

不太想上来就直说JMM啥的,我们为什么不从最简单的使用开始说起呢?这一期主播会把你当成新手教学。
我当年初学volatile的时候上来就是库库一顿原理分析,给幼小的我造成了不小的伤害,初学不需要 懂那么多🤭
具体的实现原理,我们放到下一期讲。期待的uu们,现在可以关注主播,跟上主播的节奏!!!

当你阅读dalao的框架源码的时候,你是否会见到这样一个关键字 - - - volatie,诶,你是否会好奇,为什么要加它?加了它有什么作用?
下图为bistouy使用到了volatile关键字地方:

image.png

我之前也阅读bistouy在线debug功能部分的源码输出过文章:
【Bistoury】Bistoury功能分析-在线debug

一、入门

什么是volatile关键字?

volatile 是 Java 中的一个关键字,用于修饰变量,主要作用是确保多线程环境下变量的可见性禁止指令重排序
它是 Java 内存模型(JMM, Java Memory Model)提供的一种轻量级同步机制。

volatile的作用(为什么要volatile关键字?)

  1. 保证可见性
    • 问题:在多线程环境中,每个线程都有自己的工作内存(线程栈),线程对变量的操作通常是在工作内存中进行的。如果没有同步机制,一个线程对变量的修改可能不会立即反映到主内存中,其他线程也就无法看到最新的值。
    • 解决:volatile 修饰的变量会强制线程每次读写都直接操作主内存,从而保证一个线程对变量的修改对其他线程立即可见。
  2. 禁止指令重排序
    • 问题:为了提高性能,JVM 和 CPU 可能会对指令进行重排序(Instruction Reordering)。重排序可能会导致多线程程序出现不可预期的行为。
    • 解决:volatile 修饰的变量会禁止 JVM 对其进行重排序,确保程序的执行顺序与代码的书写顺序一致。
  3. 轻量级同步
    • 问题:使用 synchronized 或锁机制会带来较大的性能开销,尤其是在只需要保证可见性和禁止重排序的场景中。
    • 解决:volatile 是一种轻量级的同步机制,性能开销较小,适用于简单的多线程同步场景。

volatie修饰位置(如何使用volatile关键字)

修饰字段:最常见的是修饰类的实例变量或静态变量,确保一个线程对变量的修改对其他线程立即可见。

public class Example {
   
    private volatile boolean flag = false;
}

(没有用但可以)修饰局部变量:虽然技术上可以修饰局部变量,但由于局部变量通常在线程栈中,每个线程有独立的副本,因此修饰局部变量没有实际意义。

public void method() {
   
    volatile int localVar = 10; // 无意义
}

修饰基本类型数组:volatile 可以修饰数组引用,但不能保证数组元素的可见性。

public class Example {
   
    private volatile int[] array;
}

修饰对象引用:可以修饰对象引用,确保引用的可见性,但不保证对象内部状态的可见性。

public class Example {
   
    private volatile MyObject obj;
}

这里,就会有好奇的小朋友要问了🙋‍,为什么修饰数组和对象的时候,不保证内部状态的可见性? 诶哟,你真的很聪明,能想到这点!!

那是因为volatile的作用范围仅限于它直接修饰的变量(在这里是数组/对象引用),而数组元素/对象字段是独立存储的,不受volatile的影响。
😄说人话就是,改引用别的线程知道,改里面的值,别的线程不知道。

public class Example {
   
    private volatile int[] array = new int[10];

    public void updateArray(int index, int value) {
   
        array[index] = value; // 对数组元素的修改不保证可见性
    }

    public void replaceArray(int[] newArray) {
   
        array = newArray; // 对数组引用的修改保证可见性
    }
}

那怎么解决勒?
对象

// 1、将对象内部字段也声明为 volatile
class MyObject {
   
    volatile int value; // 使用 volatile 修饰字段
}

// 2、修改方法,使用 synchronized 修饰
public synchronized void updateValue(int newValue) {
   
    if (obj != null) {
   
        obj.value = newValue;
    }
}

// 3、使用JUC:例如 AtomicReference、AtomicInteger 等,这些类提供了原子操作和可见性保证。
class MyObject {
   
    AtomicInteger value = new AtomicInteger(0);
}

// 4、使用final字段
// 如果对象的状态在构造后不会改变,可以将字段声明为 final,
// 这样在构造完成后,其他线程可以看到正确的值。

数组

// 1、使用 AtomicIntegerArray 或 AtomicReferenceArray,
// 这些类提供了对数组元素的原子操作和可见性保证。
public class Example {
   
    private AtomicIntegerArray array = new AtomicIntegerArray(10);

    public void updateArray(int index, int value) {
   
        array.set(index, value); // 保证可见性和原子性
    }
}

// 2、使用 synchronized 同步
public class Example {
   
    private int[] array = new int[10];

    public synchronized void updateArray(int index, int value) {
   
        array[index] = value; // 保证可见性和原子性
    }
}

// 3、使用JUC:例如,使用 CopyOnWriteArrayList 或自定义的线程安全容器
public class Example {
   
    private CopyOnWriteArrayList<Integer> list = new CopyOnWriteArrayList<>();

    public void updateList(int index, int value) {
   
        list.set(index, value); // 保证线程安全
    }
}

// 4、将数组元素声明为 volatile
class Element {
   
    volatile int value;
}

public class Example {
   
    private Element[] array = new Element[10];
}

二、volatile特性

可见性

下面的代码,我们启动了2个线程,一个负责修改cnt,一个来观察cnt的变化。我们会发现,控制台不会输出任何东西,修改cnt的线程修改的数量,对于读cnt的线程是不可见的🤗
这时候,需要我们加上volatile就可以解决了,至于为什么volatile修饰的变量会变得对其他线程可见😋,咱们下期再细说。

public class QuickTest {
   

    static int cnt;        // 需要加上volatile修饰

    public static void main(String[] args) throws Exception {
   

        // 启动读取数量的线程
        new Thread(() -> {
   
            int localCnt = cnt;
            while (true) {
   
                if (localCnt != cnt) {
   
                    System.out.println("当前值" + localCnt
                            + ",读到了修改后的数量: " + cnt);
                    localCnt = cnt;
                }
            }
        }).start();

        // 暂停1s,模拟业务操作
        try {
   
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
   
            throw new RuntimeException(e);
        }

        // 启动修改数量的线程
        new Thread(() -> {
   
            int localCnt = cnt;
            while (true) {
   
                localCnt++;
                cnt = localCnt;
                // 暂停1s,模拟业务操作
                try {
   
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
   
                    throw new RuntimeException(e);
                }
            }
        }).start();

    }
}

有序性

喜欢用单例模式的小朋友,你们好呀!我们很经典的双重检查(DCL),我们会对要创建的单例对象字段使用volatile修饰,聪明的你一定也注意到了吧。
想回忆单例设计模式,也欢迎看看这篇:【设计模式】【创建型模式】单例模式(Singleton)-CSDN博客

我们重点getInstance方法。
new Singleton();,在字节码里,是三个指令,如果发生指令重排,如下面代码所示。

public class Singleton {
   
    private static volatile Singleton instance;

    public static Singleton getInstance() {
   
        if (instance == null) {
   
            synchronized (Singleton.class) {
   
                if (instance == null) {
   
                    instance = new Singleton();
                    // ====== 下面是发生指令重排的情况 ====== 
                    // 1. 创建对象内存    Singleton instance; #3231
                    // 3. 给instance设置引用指针    instance=#3231
                    // 2. 初始化Singleton对象    new Singleton()
                }
            }
        }
        return instance;
    }

    private Singleton() {
   }
}

假设2个线程都在调用getInstance方法,线程1执行到,3.给instance设置引用指针 instance=#3231此时instance就不为null,这个时候,线程2进来,直接返回没有初始话的instance对象。这样的对象,只是一个空壳,内存空间而已,对象里面的数据还没有塞进去。

三、volatile在框架源码中运用

这里就拿我们之前说过的线程池源码来举例吧!
之前我们学习过的线程池:
【Java并发】【线程池】带你从0-1入门线程池
【源码】【Java并发】【线程池】邀请您从0-1阅读ThreadPoolExecutor源码

在线程池中,我们传入的参数,如核心现程数、最大现程数... 都有用volatile修饰

image.png

因为线程池开放了这些参数的修改方法,如下所示:

// 设置核心线程数
public void setCorePoolSize(int corePoolSize) {
   
    if (corePoolSize < 0)
        throw new IllegalArgumentException();
    int delta = corePoolSize - this.corePoolSize;
    this.corePoolSize = corePoolSize;
    if (workerCountOf(ctl.get()) > corePoolSize)
        interruptIdleWorkers();
    else if (delta > 0) {
   
        int k = Math.min(delta, workQueue.size());
        while (k-- > 0 && addWorker(null, true)) {
   
            if (workQueue.isEmpty())
                break;
        }
    }
}
// 设置最大线程数
public void setMaximumPoolSize(int maximumPoolSize) {
   
    if (maximumPoolSize <= 0 || maximumPoolSize < corePoolSize)
        throw new IllegalArgumentException();
    this.maximumPoolSize = maximumPoolSize;
    if (workerCountOf(ctl.get()) > maximumPoolSize)
        interruptIdleWorkers();
}
// 其他参数类似

当这些参数被修改的时候,我们希望所有的Worker类线程都可以感知到它的值变化。
例如,在getTask()方法中,当线程数超过corePoolSize时,会通过poll(keepAliveTime)尝试回收线程。
我们需要保证,当corePoolSize的值,被1个线程修改后,对其他线程可见。所以要加上volatile修饰。

 if (workerCount > corePoolSize) {
   
     workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS);
 }

聪明的你一定会好奇:
为什么要用volatile而不是原子类(AtomicInteger)
那是因为,corePoolSize这样的参数修改频率较低(通常仅在配置变更时),在此场景中非必需,避免不必要的性能开销。
那为什么不是synchronized?
那是因为
1、synchronized,每次读写corePoolSize都需要加锁/解锁,虽然能保证原子性,但会引入额外性能开销。
2、因为这个修改仅需保证变量的可见性(如corePoolSize的值修改后其他线程立即可见),而无需保证复合操作的原子性(例如i++这类非原子操作)。线程池中corePoolSize的更新通常为直接赋值,如setCorePoolSize()

四、总结

简单状态用volatile,原子操作靠Atomic,复合逻辑需加锁,单例模式防重排。

volatile适用场景

  1. 简单状态标志(如线程启停)。
  2. 一次性发布或双重检查锁(单例模式)。
  3. 独立观察(如配置热更新)。
  4. 需要禁止指令重排序的场景。

volatile不适用场景

  1. 复合操作(如 i++):
    需要原子性时,改用 AtomicIntegersynchronized
  2. 多变量依赖的原子操作:
    例如 if (a && b),需用锁机制保证原子性。
  3. 复杂同步逻辑:
    涉及多个步骤的线程协调时,需用 synchronizedLock

实际开发中的判断逻辑

  1. 是否需要可见性?
    • 如果一个线程修改了变量,其他线程是否需要 立即看到最新值?
    • 是 → 考虑volatile。
  2. 是否涉及指令重排序?
    • 是否存在代码执行顺序依赖(如单例模式中的对象初始化)?
    • 是 → 必须用volatile禁止重排序。
  3. 操作是否原子?
    • 是否是单一读/写操作(如boolean赋值)?
    • 是 → volatile适用。
    • 否(如i++)→ 需要锁或原子类。

后话

什么这么快结束了?你是否觉得volatile就这?
👆关注点上,下一期带你上难度,邀请与您探索Java底层深渊。
image.png

参考

volatile有哪些引用场景?
【IT老齐668】十分钟大白话volatile关键字

目录
相关文章
|
7月前
|
存储 缓存 Java
【高薪程序员必看】万字长文拆解Java并发编程!(5):深入理解JMM:Java内存模型的三大特性与volatile底层原理
JMM,Java Memory Model,Java内存模型,定义了主内存,工作内存,确保Java在不同平台上的正确运行主内存Main Memory:所有线程共享的内存区域,所有的变量都存储在主存中工作内存Working Memory:每个线程拥有自己的工作内存,用于保存变量的副本.线程执行过程中先将主内存中的变量读到工作内存中,对变量进行操作之后再将变量写入主内存,jvm概念说明主内存所有线程共享的内存区域,存储原始变量(堆内存中的对象实例和静态变量)工作内存。
240 0
|
8月前
|
缓存 安全 Java
【Java并发】【ConcurrentHashMap】适合初学体质的ConcurrentHashMap入门
ConcurrentHashMap是Java中线程安全的哈希表实现,支持高并发读写操作。相比Hashtable,它通过分段锁(JDK1.7)或CAS+synchronized(JDK1.8)实现更细粒度锁控制,提升性能与安全性。本文详细介绍其构造方法、添加/获取/删除元素等常用操作,并对比JDK1.7和1.8的区别,帮助开发者深入理解与使用ConcurrentHashMap。欢迎关注,了解更多!
451 5
【Java并发】【ConcurrentHashMap】适合初学体质的ConcurrentHashMap入门
|
8月前
|
存储 安全 Java
【Java并发】【原子类】适合初学体质的原子类入门
什么是CAS? 说到原子类,首先就要说到CAS: CAS(Compare and Swap) 是一种无锁的原子操作,用于实现多线程环境下的安全数据更新。 CAS(Compare and Swap) 的
195 15
【Java并发】【原子类】适合初学体质的原子类入门
|
8月前
|
安全 Java
【Java并发】【LinkedBlockingQueue】适合初学体质的LinkedBlockingQueue入门
前言 你是否在线程池工具类里看到过它的身影? 你是否会好奇LinkedBlockingQueue是啥呢? 没有关系,小手手点上关注,跟上主播的节奏。 什么是LinkedBlockingQueue? ...
356 1
【Java并发】【LinkedBlockingQueue】适合初学体质的LinkedBlockingQueue入门
|
8月前
|
安全 Java
【源码】【Java并发】【ArrayBlockingQueue】适合中学者体质的ArrayBlockingQueue
前言 通过之前的学习是不是学的不过瘾,没关系,马上和主播来挑战源码的阅读 【Java并发】【ArrayBlockingQueue】适合初学体质的ArrayBlockingQueue入门 还有一件事
174 5
【源码】【Java并发】【ArrayBlockingQueue】适合中学者体质的ArrayBlockingQueue
|
8月前
|
安全 Java
【Java并发】【ArrayBlockingQueue】适合初学体质的ArrayBlockingQueue入门
什么是ArrayBlockingQueue ArrayBlockingQueue是 Java 并发编程中一个基于数组实现的有界阻塞队列,属于 java.util.concurrent 包,实现了 Bl...
227 6
【Java并发】【ArrayBlockingQueue】适合初学体质的ArrayBlockingQueue入门
|
8月前
|
Java
【源码】【Java并发】【ReentrantLock】适合中学者体质的ReentrantLock源码阅读
因为本文说的是ReentrantLock源码,因此会默认,大家对AQS有基本的了解(比如同步队列、条件队列大概> 长啥样?)。 不懂AQS的小朋友们,你们好呀!也欢迎先看看这篇
184 13
【源码】【Java并发】【ReentrantLock】适合中学者体质的ReentrantLock源码阅读
|
8月前
|
监控 Java API
【Java并发】【ReentrantLock】适合初学体质的ReentrantLock入门
前言 什么是ReentrantLock? ReentrantLock 是 Java 并发包 (java.util.concurrent.locks) 中的一个类,它实现了 Lock 接口,提供了与
328 10
【Java并发】【ReentrantLock】适合初学体质的ReentrantLock入门
|
8月前
|
存储 Java
【源码】【Java并发】【ThreadLocal】适合中学者体质的ThreadLocal源码阅读
前言 下面,跟上主播的节奏,马上开始ThreadLocal源码的阅读( ̄▽ ̄)" 内部结构 如下图所示,我们可以知道,每个线程,都有自己的threadLocals字段,指向ThreadLocalMap
533 81
【源码】【Java并发】【ThreadLocal】适合中学者体质的ThreadLocal源码阅读
|
8月前
|
存储 缓存 安全
【Java并发】【ThreadLocal】适合初学体质的ThreadLocal
ThreadLocal 是 Java 中用于实现线程本地存储(Thread-Local Storage)的核心类,它允许每个线程拥有自己独立的变量副本,从而在多线程环境中实现线程隔离,避免共享变量带来的线程安全问题。
222 9
【Java并发】【ThreadLocal】适合初学体质的ThreadLocal