菜单

Administrator
发布于 2026-05-18 / 2 阅读
0
0

synchronized 和 volatile 关键字的区别

synchronized 关键字和 volatile 关键字的区别

一、前言

在 Java 并发编程中,synchronizedvolatile 是两个极其重要的关键字,它们都与多线程间的内存可见性线程安全息息相关。然而,它们的作用机制、适用场景以及底层实现有着本质的区别。本文将深入剖析这两个关键字的异同,帮助读者在实际开发中做出正确的选择。


二、Java 内存模型(JMM)基础

在理解 synchronizedvolatile 之前,必须先了解 Java 内存模型(Java Memory Model,JMM)。

JMM 规定了:

  1. 所有共享变量存储在主内存(Main Memory)中。
  2. 每个线程拥有自己的工作内存(Working Memory),线程对变量的所有操作都必须在工作内存中进行,不能直接操作主内存。
  3. 不同线程之间无法直接访问对方的工作内存,线程间变量值的传递需要通过主内存完成。

这种模型导致了三大并发问题:

  • 可见性:一个线程修改了共享变量,另一个线程能否立即看到修改?
  • 原子性:多个操作是否会被中断?
  • 有序性:编译器/处理器重排序是否会导致意外结果?

三、synchronized 关键字详解

3.1 基本概念

synchronized 是 Java 提供的一种互斥锁(Mutex)机制,它保证同一时刻最多只有一个线程可以执行被修饰的代码块或方法。

3.2 使用方式

// 1. 修饰实例方法 —— 锁是当前实例对象
public synchronized void instanceMethod() {
    // 线程安全的操作
}

// 2. 修饰静态方法 —— 锁是当前类的 Class 对象
public static synchronized void staticMethod() {
    // 线程安全的操作
}

// 3. 修饰同步代码块 —— 锁是括号内的对象
public void blockMethod() {
    synchronized (this) {
        // 线程安全的操作
    }
}

3.3 三大特性保证

特性 synchronized 的支持情况
可见性 ✅ 保证。线程释放锁前,会将工作内存中的共享变量刷新到主内存;线程获取锁时,会清空工作内存中共享变量的值,从主内存重新读取。
原子性 ✅ 保证。被 synchronized 包裹的代码块是原子操作,不会被其他线程中断。
有序性 ✅ 保证。synchronized 内的代码通过"同一锁同一时刻只有一个线程执行"来保证有序性,内部的代码也可能重排序,但由于单线程语义,结果一致(as-if-serial)。

3.4 底层实现

synchronized 在 JVM 层面通过 Monitor(监视器锁) 实现。在对象头中存储锁标记,依赖操作系统底层的 Mutex Lock 实现,因此是重量级操作。JDK 6 以后引入了锁升级机制:

无锁 → 偏向锁 → 轻量级锁 → 重量级锁

四、volatile 关键字详解

4.1 基本概念

volatile 是 Java 提供的轻量级同步机制,它主要用于保证共享变量的可见性有序性,但不保证原子性

4.2 使用方式

public class VolatileExample {
    // 保证可见性:volatile 变量的修改会立即同步到主内存
    private volatile boolean flag = false;

    public void writer() {
        flag = true;  // 写操作
    }

    public void reader() {
        if (flag) {   // 读操作
            // 一定能看到 writer 线程的修改
        }
    }
}

4.3 三大特性保证

特性 volatile 的支持情况
可见性 保证。对 volatile 变量的写操作,会立即刷新到主内存;读操作会从主内存重新读取,而不是使用工作内存中的缓存值。
原子性 不保证。对 volatile 变量的复合操作(如 count++count = count + 1)不是原子操作。
有序性 保证。通过**内存屏障(Memory Barrier)**禁止指令重排序。

4.4 底层实现

volatile 通过在指令序列中插入内存屏障来实现:

  1. 写操作:在写操作后插入 StoreStore 屏障 + StoreLoad 屏障,保证 volatile 写之前的所有普通写操作已经同步到内存,并且 volatile 写之后的代码不会重排序到写之前。
  2. 读操作:在读操作前插入 LoadLoad 屏障 + LoadStore 屏障,保证 volatile 读之后的所有普通读操作不会重排序到读之前。

4.5 volatile 的典型使用场景

// 场景一:状态标志位
volatile boolean shutdown = false;
// 线程1
public void shutdown() { shutdown = true; }
// 线程2
public void run() {
    while (!shutdown) {
        // do work
    }
}

// 场景二:Double-Checked Locking(单例模式)
public class Singleton {
    private static volatile Singleton instance;
    private Singleton() {}
    public static Singleton getInstance() {
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

五、核心区别对比

对比维度 synchronized volatile
性质 互斥锁(重量级) 轻量级同步机制
可见性 保证(通过锁的释放/获取) 保证(通过内存屏障)
原子性 保证(代码块级原子性) 不保证(仅保证读写操作的单个原子性)
有序性 保证(as-if-serial) 保证(禁止指令重排序)
是否阻塞 是(线程阻塞/唤醒) 否(线程不阻塞)
是否可修饰 方法、代码块 仅变量
开销 较高(涉及线程上下文切换) 较低(无锁,仅内存屏障)
能否修饰 null 可以锁定任意对象 不能修饰 null 值

六、代码对比示例

6.1 volatile 无法保证原子性

public class VolatileNotAtomic {
    private static volatile int count = 0;

    public static void main(String[] args) throws InterruptedException {
        Thread[] threads = new Thread[10];
        for (int i = 0; i < 10; i++) {
            threads[i] = new Thread(() -> {
                for (int j = 0; j < 1000; j++) {
                    count++;  // 非原子操作:读-改-写
                }
            });
            threads[i].start();
        }
        for (Thread t : threads) {
            t.join();
        }
        System.out.println("count = " + count);
        // 期望值:10000,实际值可能小于 10000
    }
}

输出示例(每次运行可能不同):

count = 9987

6.2 synchronized 保证原子性

public class SynchronizedAtomic {
    private static int count = 0;

    public static synchronized void increment() {
        count++;  // 原子操作
    }

    public static void main(String[] args) throws InterruptedException {
        Thread[] threads = new Thread[10];
        for (int i = 0; i < 10; i++) {
            threads[i] = new Thread(() -> {
                for (int j = 0; j < 1000; j++) {
                    increment();
                }
            });
            threads[i].start();
        }
        for (Thread t : threads) {
            t.join();
        }
        System.out.println("count = " + count);
        // 始终输出:10000
    }
}

输出(始终正确):

count = 10000

6.3 volatile 保证可见性

public class VolatileVisibility {
    private static volatile boolean running = true;

    public static void main(String[] args) throws InterruptedException {
        Thread worker = new Thread(() -> {
            System.out.println("工作线程开始执行...");
            while (running) {
                // volatile 保证能及时看到主线程的修改
            }
            System.out.println("工作线程收到停止信号,退出。");
        });
        worker.start();

        Thread.sleep(1000);
        running = false;  // 主线程修改标志位
        System.out.println("主线程已发送停止信号。");
    }
}

输出

工作线程开始执行...
主线程已发送停止信号。
工作线程收到停止信号,退出。

注意:如果去掉 volatile,工作线程可能永远无法看到 running 的修改,从而陷入死循环。


七、常见面试问题

Q1: synchronized 和 volatile 有什么区别?

答: 这是面试中最基础的问题。主要区别如下:

  1. 性质不同synchronized 是互斥锁,volatile 是轻量级同步机制。
  2. 原子性synchronized 保证代码块的原子性;volatile 不保证复合操作的原子性。
  3. 使用范围synchronized 可修饰方法和代码块;volatile 只能修饰变量。
  4. 阻塞synchronized 会导致线程阻塞;volatile 不会。
  5. 开销synchronized 开销较大(涉及锁竞争、线程上下文切换);volatile 开销较小。
  6. 底层实现synchronized 基于 Monitor 和操作系统 Mutex;volatile 基于内存屏障。

Q2: volatile 能保证原子性吗?如果不能,请举例说明。

答: 不能。volatile 只能保证单个读/写操作的原子性,但无法保证 count++ 这类"读-改-写"复合操作的原子性。例如 count++ 实际上包含三个步骤:

  1. 读取 count 的值
  2. count + 1
  3. 将结果写回 count

这三个步骤之间可能被其他线程中断,导致数据不一致。具体示例见上文 6.1 volatile 无法保证原子性

Q3: volatile 和 synchronized 各自是如何保证可见性的?

答:

  • synchronized:在线程进入 synchronized 代码块时,会清空工作内存中共享变量的值,强制从主内存重新加载;在线程退出 synchronized 代码块时,会将对共享变量的修改刷新到主内存中。
  • volatile:对 volatile 变量的写操作会立即刷新到主内存,读操作会直接从主内存读取,而不是从工作内存的缓存中读取。

Q4: 什么场景下应该用 volatile 而不是 synchronized?

答: 以下场景优先考虑 volatile

  1. 状态标志位:如 volatile boolean shutdown,只有一个线程写,多个线程读。
  2. Double-Checked Locking 单例模式:防止指令重排序导致返回未完全初始化的对象。
  3. 纯赋值操作:如 volatile int x = 1,不依赖当前值的复合操作。
  4. 读多写少且写操作不依赖当前值的场景。

如果涉及复合操作(如 count++)或需要保证多个操作的原子性,则必须使用 synchronizedLock

Q5: volatile 关键字的内存屏障是如何工作的?

答: volatile 通过在指令序列中插入内存屏障来禁止指令重排序:

  • volatile 写操作

    • 在写操作前插入 StoreStore 屏障,禁止前面的普通写与后面的 volatile 写重排序。
    • 在写操作后插入 StoreLoad 屏障,禁止 volatile 写与后面的读操作重排序,并保证写入对其他线程可见。
  • volatile 读操作

    • 在读操作后插入 LoadLoad 屏障,禁止后面的普通读与 volatile 读重排序。
    • 在读操作后插入 LoadStore 屏障,禁止后面的写操作与 volatile 读重排序。

Q6: 为什么 DCL 单例模式中 instance 必须加 volatile?

答: 因为 instance = new Singleton() 在 JVM 层面不是原子操作,它大致分为三步:

  1. 分配内存空间
  2. 初始化对象(调用构造方法)
  3. 将引用指向分配的内存地址

如果不加 volatile,JVM 可能将步骤 2 和 3 重排序为 1→3→2,导致另一个线程在步骤 3 之后、步骤 2 之前读取到 instance 不为 null,从而返回一个还未构造完成的对象,引发问题。volatile 通过禁止指令重排序保证了先初始化后赋值。

private static volatile Singleton instance;

Q7: synchronized 在 JDK 6 之后有哪些优化?

答: JDK 6 对 synchronized 进行了大量优化,引入了:

  1. 偏向锁:无竞争时,偏向第一个获得锁的线程,减少 CAS 开销。
  2. 轻量级锁:通过 CAS 自旋获取锁,避免线程阻塞。
  3. 锁消除:JIT 编译器检测到不可能存在共享数据竞争时,消除锁。
  4. 锁粗化:将多个连续的加锁-解锁操作合并为一个更大的锁范围。
  5. 自适应自旋:根据上次自旋的成功率动态调整自旋次数。

锁的升级过程为:无锁 → 偏向锁 → 轻量级锁 → 重量级锁,且锁只能升级不能降级。

Q8: 有了 synchronized 为什么还需要 volatile?

答: 虽然 synchronized 能同时保证可见性、原子性和有序性,但它在某些场景下过于"重量级":

  1. 性能开销:synchronized 涉及线程阻塞、上下文切换,而 volatile 不需要。
  2. 使用简便:volatile 只需在变量前加关键字,无需包裹代码块。
  3. 死锁风险:synchronized 使用不当可能导致死锁,volatile 没有锁自然没有死锁问题。
  4. 适用场景不同:对于单纯需要可见性的场景(如状态标志位),volatile 是最轻量、最合适的选择。

八、总结

维度 synchronized volatile
可见性 ✅ JMM 规范保证 ✅ 内存屏障保证
原子性 ✅ 代码块级别 ❌ 仅单次读写
有序性 ✅ as-if-serial ✅ 禁止重排序
阻塞 ✅ 会阻塞 ❌ 不会阻塞
开销 较高 较低
适用场景 复合操作、互斥访问 状态标志位、可见性保障

选择建议

  • 需要原子性互斥访问 → 使用 synchronized
  • 需要可见性且操作是单一赋值 → 使用 volatile
  • 仅靠 volatile 无法保证线程安全时,不要犹豫,使用 synchronizedLockAtomic

参考资料

  • 《Java 并发编程的艺术》
  • 《深入理解 Java 虚拟机》(第3版)
  • Oracle Java Language Specification

评论