Java中 synchronized 和 volatile的核心区别解析
作者:木易 士心
概述
Java 并发编程中的两个核心关键字:synchronized 和 volatile。它们都是为了解决多线程环境下的数据一致性问题,但在作用机制、保证的特性以及适用场景上有着本质的区别。
简单来说:
synchronized 是一把“重量级的锁”,它通过互斥访问来保证原子性、可见性和有序性。
volatile 是一个“轻量级的同步机制”,它主要保证可见性和有序性,但不保证原子性。
1. synchronized 关键字详解
synchronized 是 Java 中最基础、最常用的同步机制,它通过获取和释放对象的“监视器锁”(Monitor Lock)来实现线程间的互斥访问。
1.1 作用与核心特性
- 互斥性 (Mutual Exclusion)
这是 synchronized 最核心的作用。它确保在同一时刻,只有一个线程能够执行被 synchronized 保护的代码块或方法。其他试图进入的线程会被阻塞,直到当前线程释放锁。 - 原子性 (Atomicity)
由于互斥性,被 synchronized 保护的代码块被视为一个不可分割的整体。线程要么执行完整个代码块,要么完全不执行,不会被其他线程打断。这保证了复合操作(如 i++)的原子性。 - 可见性 (Visibility)
synchronized 不仅提供互斥,还保证了内存可见性。根据 Java 内存模型 (JMM) 的规定:
进入 synchronized 块时:线程会清空其工作内存中共享变量的副本,强制从主内存重新加载最新的值。退出 synchronized 块时:线程会将其工作内存中对共享变量的修改强制刷新回主内存。
这样,一个线程在临界区内对变量的修改,对下一个进入该临界区的线程是立即可见的。 - 有序性 (Ordering)
synchronized 通过“一个变量在同一时刻只允许一个线程对其进行 lock 操作”的规则,天然地禁止了指令重排序。在 synchronized 块内部,代码的执行顺序与程序的书写顺序一致。
1.2. 使用方式
synchronized 可以修饰方法或代码块,锁定的对象不同,其作用范围也不同。
1.2.1 修饰实例方法 (非静态方法)
public class Counter { private int count = 0; // 锁定的是当前对象实例 (this) public synchronized void increment() { count++; // 这个操作是原子的 } public synchronized int getCount() { return count; } }
锁对象 当前对象实例 (this)。
作用范围 同一个对象实例的多个 synchronized 实例方法之间是互斥的。不同对象实例的 synchronized 方法可以并发执行。
1.2.2 修饰静态方法
public class GlobalCounter { private static int globalCount = 0; // 锁定的是当前类的 Class 对象 (GlobalCounter.class) public static synchronized void incrementGlobal() { globalCount++; } public static synchronized int getGlobalCount() { return globalCount; } }
锁对象 该类的 Class 对象。
作用范围 无论创建多少个类的实例,所有线程在调用该类的 synchronized 静态方法时,都会竞争同一把锁,实现全局互斥。
1.2.3 修饰代码块 (Synchronized Block)
public class FineGrainedCounter { private int countA = 0; private int countB = 0; private final Object lockA = new Object(); private final Object lockB = new Object(); // 只锁定操作 countA 的部分,提高并发度 public void incrementA() { synchronized (lockA) { // 锁定指定的对象 lockA countA++; } } // 只锁定操作 countB 的部分 public void incrementB() { synchronized (lockB) { // 锁定指定的对象 lockB countB++; } } // 锁定当前对象实例 public void doSomething() { synchronized (this) { // ... 临界区代码 } } }
锁对象 synchronized 括号内指定的任意对象。
作用范围 灵活性最高。可以精确控制需要同步的代码范围,避免将整个方法都锁定,从而减少锁的竞争,提高并发性能。
1.3. 实现原理
JVM 通过对象内部的“监视器锁”(Monitor)来实现 synchronized。在字节码层面:
- 进入 synchronized 代码块时,会执行 monitorenter 指令。
- 退出 synchronized 代码块(正常退出或发生异常)时,会执行 monitorexit 指令。
为了优化性能,JDK 1.6 引入了锁升级机制:
- 无锁状态
- 偏向锁 (Biased Locking)
针对只有一个线程访问同步块的场景,将锁偏向于该线程,减少不必要的 CAS 操作。 - 轻量级锁 (Lightweight Locking)
当有第二个线程竞争时,升级为轻量级锁,通过自旋 CAS 尝试获取锁,避免线程阻塞。 - 重量级锁 (Heavyweight Locking)
当自旋一定次数后仍未获取到锁,或有多个线程竞争时,升级为重量级锁,线程会被挂起,进入阻塞状态。
1.4. 优缺点
- 优点
功能强大,能同时保证原子性、可见性和有序性。
使用简单,是解决并发问题的首选方案。
支持重入,同一个线程可以多次获取同一把锁。 - 缺点
性能开销: 获取和释放锁需要操作系统介入,可能导致线程上下文切换,带来性能损耗。
可能导致死锁: 如果多个线程以不同的顺序获取多个锁,可能会发生死锁。
阻塞: 未获取到锁的线程会被阻塞,无法做其他事情。
1.5. 适用场景
适用于需要对共享资源进行复杂操作、保证操作原子性的场景,例如:
- 银行转账(需要保证扣款和加款两个操作的原子性)。
- 计数器的递增 (i++)。
- 对集合进行增删改查操作。
2. volatile 关键字详解
volatile 是一个变量修饰符,它不提供任何互斥机制,而是通过内存屏障(Memory Barrier)来保证变量的可见性和禁止指令重排序。
2.1 作用与核心特性
- 可见性 (Visibility): 这是 volatile 最主要的作用。
- 当一个线程修改了 volatile 变量的值,这个新值会立即被写入主内存。
- 当其他线程读取这个 volatile 变量时,会强制从主内存中读取最新的值,而不是使用自己工作内存中的缓存副本。
- 这样就保证了所有线程看到的都是该变量的最新值。
- 有序性 (Ordering) volatile 通过插入内存屏障来禁止指令重排序。
- 在写一个 volatile 变量之前,JVM 会插入一个 StoreStore 屏障,确保之前的普通写操作都已完成。
- 在写一个 volatile 变量之后,JVM 会插入一个 StoreLoad 屏障,确保写操作对其他处理器可见。
- 在读一个 volatile 变量之前,JVM 会插入一个 LoadLoad 屏障,确保读取到的是最新值。
- 在读一个 volatile 变量之后,JVM 会插入一个 LoadStore 屏障,确保后续的普通写操作不会被重排序到读操作之前。
- 这保证了 volatile 变量的读写操作不会被重排序,并且建立了“happens-before”关系。
- 不保证原子性 (No Atomicity)
- volatile 无法保证复合操作的原子性。例如,volatile int count = 0; 语句 count++ 看起来是一条语句,但在底层是“读取-修改-写入”三个步骤。即使 count 是 volatile 的,多个线程同时执行 count++ 时,依然可能出现竞态条件,导致最终结果小于预期。
2.2. 使用方式
volatile 只能用来修饰变量。
public class VolatileExample { // 修饰一个布尔标志位,用于线程间通信 private volatile boolean shutdownRequested = false; // 修饰一个对象引用 private volatile Config config; // 线程A:设置标志位 public void shutdown() { shutdownRequested = true; // 写操作,会立即刷新到主内存 } // 线程B:检查标志位 public void doWork() { while (!shutdownRequested) { // 读操作,每次都从主内存读取最新值 // ... 执行任务 } // 收到关闭请求,优雅退出 } // 注意:以下操作不是原子的! private volatile int counter = 0; public void unsafeIncrement() { counter++; // 读-改-写,非原子操作,多线程下结果可能错误 } }
2.3 实现原理
volatile 的实现主要依赖于 CPU 的缓存一致性协议(如 MESI)和 JVM 插入的内存屏障指令。它告诉 JVM 和 CPU,这个变量是“易变的”,不能对其进行激进的优化(如缓存、重排序)。
2.4. 优缺点
- 优点
轻量级: 相比 synchronized,开销非常小,不会引起线程阻塞。
保证可见性和有序性: 适用于简单的状态标志传递。 - 缺点
不保证原子性: 无法用于需要原子操作的场景。
功能有限: 只能用于变量,不能用于方法或代码块。
2.5. 适用场景
适用于“一个线程写,多个线程读”,且写操作是原子的(通常是直接赋值)的场景:
- 状态标志位 如上面例子中的 shutdownRequested,用于通知其他线程停止工作。
- 一次性安全发布 (Safe Publication) 在对象构造完成后,通过 volatile 引用发布,可以保证其他线程看到的是完全构造好的对象。
- 双重检查锁定 (DCL) 的单例模式 在单例模式中,volatile 用于防止指令重排序导致其他线程拿到一个未完全初始化的对象。
public class Singleton { // volatile 防止 instance = new 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; } }
3 总结
3.1 synchronized 与 volatile 的核心区别
特性 | synchronized | volatile |
---|---|---|
作用对象 | 方法、代码块 | 变量 |
核心机制 | 互斥锁 (Monitor) | 内存屏障 (Memory Barrier) |
原子性 | 保证 (通过互斥实现) | 不保证 (仅保证单次读/写原子) |
可见性 | 保证 (进出同步块时刷新主内存) | 保证 (强制读写主内存) |
有序性 | 保证 (通过互斥和禁止重排序) | 保证 (通过内存屏障禁止重排序) |
线程阻塞 | 会阻塞 (未获取锁的线程进入阻塞状态) | 不会阻塞 (线程可以继续执行) |
性能开销 | 较大 (涉及操作系统,可能上下文切换) | 较小 (主要是内存屏障开销) |
适用场景 | 复杂的原子操作、临界区保护 简单的状态标志、 | 一次性安全发布、DCL单例模式 |
3.2 适用场景
3.2.1 状态标志控制 使用volatile
仅需保证可见性进需要操作是原子的 (如 flag = true): 优先使用 volatile,因为它更轻量。
class TaskRunner { private volatile boolean stopped = false; // 线程安全的状态标志 public void run() { while (!stopped) { /* 执行任务 */ } } public void stop() { stopped = true; } // 修改立即可见 }
3.2.2 单例模式(双重检查锁定)synchronized+volatile
volatile防止new Singleton()的分解步骤重排序,避免返回未初始化的对象
class Singleton { private static volatile Singleton instance; // 禁止指令重排序 public static Singleton getInstance() { if (instance == null) { synchronized (Singleton.class) { if (instance == null) { instance = new Singleton(); // 禁止重排序:分配内存→初始化→赋值引用 } } } return instance; } }
3.2.3 临界区保护 使用synchronized
强制原子性,适合需要互斥访问的复合操作(如读写共享变量)。
class BankAccount { private double balance; public synchronized void deposit(double amount) { // 整个方法同步 balance += amount; } public void withdraw(double amount) { synchronized (this) { // 代码块同步 balance -= amount; } } }
3.2.4 线程协作(等待/通知机制)
synchronized提供锁的获取/释放机制,配合wait()/notifyAll()实现线程间协作。
class ProducerConsumer { private final Object lock = new Object(); private boolean isProduced = false; public void produce() { synchronized (lock) { while (isProduced) { lock.wait(); } // 等待消费 // 生产数据... isProduced = true; lock.notifyAll(); // 通知消费者 } } public void consume() { synchronized (lock) { while (!isProduced) { lock.wait(); } // 等待生产 // 消费数据... isProduced = false; lock.notifyAll(); // 通知生产者 } } }
到此这篇关于Java中 synchronized 和 volatile的核心区别解析的文章就介绍到这了,更多相关Java synchronized 和 volatile内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!