volatile
是 Java 提供的一个轻量级同步机制,用于保证变量在多个线程之间的可见性。它可以修饰变量(但不能修饰方法或代码块),告诉 Java 虚拟机对该变量的操作要遵循特定的规则。
主要功能
- 内存可见性
- 当一个线程修改了被
volatile
修饰的变量,其值会立即更新到主内存中,其他线程能够立刻看到最新值。
- 当一个线程修改了被
- 禁止指令重排序
- 使用
volatile
修饰的变量,会在读和写操作时加入内存屏障,防止指令重排序。这可以确保变量的赋值操作不会被编译器或 CPU 优化到其他位置。
- 使用
内存可见性
Java 内存模型(Java Memory Model,简称 JMM)旨在屏蔽不同硬件和操作系统之间的内存访问差异。由于不同的硬件架构和操作系统在内存访问机制上存在差异,这可能导致相同的 Java 代码在不同平台上表现出不同的行为。JMM 的作用是确保这些差异不会影响程序的正确性,使得相同的代码在不同平台上的行为保持一致。
根据 JMM 的规定:
- 所有实例变量和静态变量都存储在主内存中。
- 每个线程都有自己的工作内存,其中保存了该线程使用的变量的副本。
- 线程对变量的所有读写操作都在其工作内存中进行,而不是直接操作主内存中的变量。
这种设计确保了多线程环境下的内存可见性和数据一致性。
然而,Java 内存模型会带来一个新的问题,那就是内存可见性问题,也就是当某个线程修改了主内存中共享变量的值之后,其他线程不能感知到此值被修改了,它会一直使用自己工作内存中的“旧值”,这样程序的执行结果就不符合我们的预期了,这就是内存可见性问题,我们用以下代码来演示一下这个问题:
package com.guide.fullstack;
public class CompassExample {
// 定义一个表示指南针状态的标志
private static boolean pointingNorth = false;
public static void main(String[] args) {
// 模拟指南针的方向监测线程
Thread compassMonitor = new Thread(() -> {
System.out.println("指南针监测线程启动...");
while (!pointingNorth) {
// 一直等待指南针指向北方
}
System.out.println("指南针已经指向北方!");
});
// 模拟设置指南针方向的线程
Thread compassSetter = new Thread(() -> {
try {
Thread.sleep(2000); // 模拟设置方向需要一定时间
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("设置指南针指向北方...");
pointingNorth = true; // 修改状态
});
// 启动两个线程
compassMonitor.start();
compassSetter.start();
}
}
运行这个程序会发现输出永远停留在 指南针监测线程启动...
,因为第一个线程永远感知不到第二个线程对于变量值的修改
禁止指令重排序
指令重排序是指编译器或 CPU 为了优化程序的执行性能,而对指令进行重新排序的一种手段。
指令重排序的实现初衷是好的,但是在多线程执行中,如果执行了指令重排序可能会导致程序执行出错。指令重排序最典型的一个问题就发生在单例模式中,比如以下问题代码:
public class CommonUtil {
// 单例模式
private static CommonUtil instance;
private CommonUtil() {
}
public static CommonUtil getInstance() {
if (instance == null) {
synchronized (CommonUtil.class) {
if (instance == null) {
instance = new CommonUtil();
}
}
}
return instance;
}
}
以上问题发生在代码这一行instance = new CommonUtil();
,这行代码看似只是一个创建对象的过程,然而它的实际执行却分为以下 3 步:
- 创建内存空间。
- 在内存空间中初始化对象 CommonUtil。
- 将内存地址赋值给 instance 对象(执行了此步骤,instance 就不等于 null 了)。
如果此变量不加 volatile,那么线程 1 在执行到上述代码时就可能会执行指令重排序,将原本是 1、2、3 的执行顺序,重排为 1、3、2。但是特殊情况下,线程 1 在执行完第 3 步之后,如果来了线程 2 执行到上述代码,判断 instance 对象已经不为 null,但此时线程 1 还未将对象实例化完,那么线程 2 将会得到一个被实例化“一半”的对象,从而导致程序执行出错
为什么日常编程中使用得不多
无法保证原子性
volatile只保证可见性,不能保证操作的原子性。例如:
private volatile int count = 0;
public void increment() {
count++; // 非原子操作,可能产生竞态条件
}
这种情况需要使用 synchronized
或AtomicInteger
来替代。
性能问题
- 虽然
volatile
比锁轻量级,但仍会引入内存屏障,影响性能。因此,除非确实需要,通常避免使用。
使用场景受限
- 只有在变量状态的变化与其他变量无关,并且不涉及复杂的并发逻辑时,
volatile
才适用。更多情况下,锁或线程安全工具类(如java.util.concurrent
提供的工具)更为灵活和可靠。
总结
volatile
是一个简单但重要的关键字,它为并发编程提供了基本的内存可见性保证。然而,由于其功能局限性,在日常开发中常常被更强大的工具(如 synchronized
、Lock
或并发包)取代。volatile
更适合在需要低延迟且轻量同步的场景中使用,或者作为标志位和简单状态管理的工具。
欢迎关注公众号:“全栈开发指南针”
这里是技术潮流的风向标,也是你代码旅程的导航仪!🚀
Let’s code and have fun! 🎉