
在 Java 并发编程中,volatile和synchronized是保证线程安全的两大核心关键字。它们如同两把钥匙,分别应对不同场景下的并发问题,但很多开发者对其底层原理和适用场景一知半解,导致使用时频繁踩坑。本文将从原理到实践,全面解析这两个关键字的本质区别与协同作用。
volatile是 Java 提供的最轻量级的同步机制,它的核心作用是保证变量的 “可见性” 和 “有序性”,但不保证原子性。
在多线程环境中,每个线程都有自己的工作内存(高速缓存的抽象),变量的读取和修改会先在工作内存中进行,再同步到主内存。当一个线程修改了共享变量的值,其他线程可能因未及时读取主内存的新值而导致数据不一致 —— 这就是 “可见性问题”。
volatile的作用正是强制线程每次读取变量时都从主内存获取最新值,修改后立即同步回主内存,确保所有线程看到的变量值是一致的。
public class VolatileDemo { // 用volatile修饰共享变量 private static volatile boolean flag = false; public static void main(String[] args) throws InterruptedException { new Thread(() -> { while (!flag) { // 循环等待flag变为true } System.out.println("线程1检测到flag变化"); }).start(); Thread.sleep(1000); // 主线程修改flag flag = true; System.out.println("主线程修改flag为true"); }}若flag不加volatile,线程 1 可能永远无法退出循环(因未感知到主内存的变化);加了volatile后,线程 1 会立即感知到变化并退出。
有序性指程序执行的顺序与代码顺序一致。但编译器或 CPU 为了优化性能,可能对指令进行 “重排序”,在单线程中这没问题,但多线程中可能导致逻辑错误。
volatile通过禁止指令重排序保证有序性。例如,在双重检查锁单例模式中,volatile修饰的实例变量可避免因重排序导致的空指针问题:
public class Singleton { // 必须用volatile修饰,防止指令重排序 private static volatile Singleton instance; private Singleton() {} public static Singleton getInstance() { if (instance == null) { // 第一次检查 synchronized (Singleton.class) { if (instance == null) { // 第二次检查 // 若不加volatile,可能发生"半初始化"问题 instance = new Singleton(); } } } return instance; }}new Singleton()可分解为 3 步:分配内存→初始化对象→赋值给引用。若发生重排序,可能出现 “赋值先于初始化”,导致其他线程拿到未初始化的对象。volatile禁止了这种重排序。
volatile无法解决多线程对变量的 “复合操作” 原子性问题。例如i++看似简单,实则包含 “读取 - 修改 - 写入” 三步,多线程并发时仍会出现数据不一致:
public class VolatileAtomicDemo { private static volatile int count = 0; public static void main(String[] args) throws InterruptedException { Runnable task = () -> { for (int i = 0; i < 1000; i++) { count++; // 非原子操作,volatile无法保证线程安全 } }; Thread t1 = new Thread(task); Thread t2 = new Thread(task); t1.start(); t2.start(); t1.join(); t2.join(); System.out.println("count最终值:" + count); // 可能小于2000 }}上述代码中,count加了volatile,但最终结果仍可能小于 2000,因为count++的三步操作可能被线程交替执行。
synchronized是 Java 中最常用的重量级同步机制,它通过 “加锁 - 解锁” 的方式,保证同一时刻只有一个线程能执行特定代码块,从而解决可见性、有序性和原子性问题。
public class SynchronizedDemo { // 1. 修饰实例方法 public synchronized void instanceMethod() { // 临界区代码 } // 2. 修饰静态方法 public static synchronized void staticMethod() { // 临界区代码 } // 3. 修饰代码块 public void blockMethod() { synchronized (this) { // 锁对象为当前实例 // 临界区代码 } }}synchronized的实现依赖于对象头中的 Mark Word和监视器锁(Monitor):
锁升级过程(JDK 6 + 的优化):
synchronized不仅保证原子性,还隐含着与volatile类似的内存语义:
这意味着synchronized天然解决了可见性问题,同时因互斥执行保证了有序性。
特性 | volatile | synchronized |
|---|---|---|
原子性 | 不保证(仅修饰单个变量的读 / 写) | 保证(临界区代码的原子执行) |
可见性 | 保证(强制读写主内存) | 保证(解锁时同步主内存) |
有序性 | 保证(禁止重排序) | 保证(互斥执行 + 隐含内存屏障) |
性能开销 | 轻量级(无锁) | 重量级(可能升级为 Monitor 锁) |
使用场景 | 多线程读、单线程写的变量 | 多线程读写的临界区代码 |
是否可中断 | 不可中断 | 不可中断(除非设置超时) |
是否可重入 | 无锁概念,不存在重入 | 可重入(同一线程可重复获取锁) |
在生产者 - 消费者模式中,volatile可用于标记队列状态(空 / 满),synchronized用于保证队列操作的原子性:
public class ProducerConsumer { private final Queue<Integer> queue = new LinkedList<>(); private static final int MAX_SIZE = 10; // 用volatile标记队列状态(也可通过synchronized实现,但前者更轻量) private volatile boolean isRunning = true; public void produce(int value) throws InterruptedException { synchronized (queue) { while (queue.size() == MAX_SIZE) { queue.wait(); // 队列满时等待 } queue.add(value); queue.notifyAll(); // 通知消费者 } } public int consume() throws InterruptedException { synchronized (queue) { while (queue.isEmpty()) { queue.wait(); // 队列空时等待 } int value = queue.poll(); queue.notifyAll(); // 通知生产者 return value; } } public void stop() { isRunning = false; // 线程安全的状态修改 }}错。volatile仅解决可见性和有序性,无法保证原子性。对于i++这类复合操作,必须用synchronized或原子类(如AtomicInteger)。
不完全对。JDK 6 后引入的锁升级机制(偏向锁、轻量级锁)大幅降低了synchronized的开销,在低竞争场景下性能接近volatile。
不必。仅当变量被多个线程同时访问且至少有一个线程修改时,才需要同步机制。
volatile和synchronized是 Java 并发编程的基础,二者并非对立关系,而是互补的工具:
理解它们的底层原理(内存模型、锁机制)是正确使用的前提。在实际开发中,应根据场景选择合适的工具,必要时让它们协同工作,才能写出高效且安全的并发代码。
记住:没有最好的同步机制,只有最适合的场景。深入理解并发的本质,才能在多线程世界中游刃有余。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。