👋hi,我不是一名外包公司的员工,也不会偷吃茶水间的零食,我的梦想是能写高端CRUD
🔥 2025本人正在沉淀中… 博客更新速度++
👍 欢迎点赞、收藏、关注,跟上我的更新节奏
📚欢迎订阅专栏,专栏别名《在2B工作中寻求并发是否搞错了什么》
前言
不太想上来就直说JMM啥的,我们为什么不从最简单的使用开始说起呢?这一期主播会把你当成新手教学。
我当年初学volatile的时候上来就是库库一顿原理分析,给幼小的我造成了不小的伤害,初学不需要 懂那么多🤭
具体的实现原理,我们放到下一期讲。期待的uu们,现在可以关注主播,跟上主播的节奏!!!
当你阅读dalao的框架源码的时候,你是否会见到这样一个关键字 - - - volatie
,诶,你是否会好奇,为什么要加它?加了它有什么作用?
下图为bistouy使用到了volatile关键字地方:
我之前也阅读bistouy在线debug功能部分的源码输出过文章:
【Bistoury】Bistoury功能分析-在线debug
一、入门
什么是volatile关键字?
volatile
是 Java 中的一个关键字,用于修饰变量,主要作用是确保多线程环境下变量的可见性和禁止指令重排序。
它是 Java 内存模型(JMM, Java Memory Model)提供的一种轻量级同步机制。
volatile的作用(为什么要volatile关键字?)
- 保证可见性
- 问题:在多线程环境中,每个线程都有自己的工作内存(线程栈),线程对变量的操作通常是在工作内存中进行的。如果没有同步机制,一个线程对变量的修改可能不会立即反映到主内存中,其他线程也就无法看到最新的值。
- 解决:volatile 修饰的变量会强制线程每次读写都直接操作主内存,从而保证一个线程对变量的修改对其他线程立即可见。
- 禁止指令重排序
- 问题:为了提高性能,JVM 和 CPU 可能会对指令进行重排序(Instruction Reordering)。重排序可能会导致多线程程序出现不可预期的行为。
- 解决:volatile 修饰的变量会禁止 JVM 对其进行重排序,确保程序的执行顺序与代码的书写顺序一致。
- 轻量级同步
- 问题:使用 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)-优快云博客
我们重点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修饰
因为线程池开放了这些参数的修改方法,如下所示:
// 设置核心线程数
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适用场景
- 简单状态标志(如线程启停)。
- 一次性发布或双重检查锁(单例模式)。
- 独立观察(如配置热更新)。
- 需要禁止指令重排序的场景。
volatile不适用场景
- 复合操作(如 i++):
需要原子性时,改用AtomicInteger
或synchronized
。 - 多变量依赖的原子操作:
例如 if (a && b),需用锁机制保证原子性。 - 复杂同步逻辑:
涉及多个步骤的线程协调时,需用synchronized
或Lock
。
实际开发中的判断逻辑
- 是否需要可见性?
- 如果一个线程修改了变量,其他线程是否需要 立即看到最新值?
- 是 → 考虑volatile。
- 是否涉及指令重排序?
- 是否存在代码执行顺序依赖(如单例模式中的对象初始化)?
- 是 → 必须用volatile禁止重排序。
- 操作是否原子?
- 是否是单一读/写操作(如boolean赋值)?
- 是 → volatile适用。
- 否(如i++)→ 需要锁或原子类。
后话
什么这么快结束了?你是否觉得volatile就这?
👆关注点上,下一期带你上难度,邀请与您探索Java底层深渊。