一、核心概念:可见性、有序性
在 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 会重排序:
- 分配对象内存空间;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(轻量级,效率更高),示例参考二、4 的VolatileHappensBeforeDemo。
五、volatile 与 synchronized 对比
volatile和synchronized都是解决并发可见性和有序性的手段,但特性和使用场景差异显著,volatile 是轻量级同步,synchronized 是重量级同步,对比如下:
| 特性 | volatile | synchronized |
|---|---|---|
| 修饰范围 | 仅能修饰成员变量 / 实例变量 | 可修饰方法、代码块、静态方法 |
| 可见性 | 保证 | 保证 |
| 有序性 | 保证 | 保证(通过监视器锁规则) |
| 原子性 | 不保证 | 保证(独占锁,串行执行) |
| 性能 | 轻量级,无锁,开销极小 | 重量级,有锁竞争,开销大 |
| 阻塞性 | 非阻塞式同步 | 阻塞式同步(锁等待) |
核心使用原则
- 若仅需状态标记 、单例 DCL 、普通变量的可见性 ,使用
volatile,效率更高; - 若需原子操作 (如
i++)、多步操作的同步 ,使用synchronized或 JUC 的Lock、原子类; volatile可作为synchronized的补充,而非替代。
六、学习总结
1. 核心总结
volatile的核心价值:轻量级保证可见性和有序性,是 JMM 中最常用的同步手段之一;volatile的底层实现:内存屏障 (禁止重排序)+主内存直读直写(保证可见性);volatile的局限性:不保证原子性,无法解决多线程写操作的竞态问题;- 关键关联:
volatile的特性通过happens-before 的volatile 变量规则 和传递性规则保障,是 JUC 并发编程的基础。
2. 实用使用建议
- 优先使用
volatile作为状态标记位,这是其最安全、最典型的场景; - 实现DCL 单例模式 时,必须给实例引用加
volatile,禁止指令重排序; - 不要用
volatile修饰需要多线程原子写的变量,需配合原子类或锁; - 利用
volatile的传递性,可实现普通变量的跨线程可见,替代重量级锁; - JUC 原子类底层已使用
volatile,无需额外修饰,直接使用即可; - 区分可见性 / 有序性和原子性:若问题是读不到最新值 /指令重排序 ,用
volatile;若问题是多线程写冲突,用锁 / 原子类。