【Java并发】volatile 与 synchronized 关键字
☕深入理解 Java 的并发关键字:volatile 与 synchronized
在多线程编程中,保证数据一致性、可见性和有序性是极为重要的。Java 提供了两种基础的并发控制关键字:volatile
和 synchronized
。这两个关键字虽常被提及,但很多开发者对它们的底层原理和使用场景仍有误解。
🧱 一、线程安全的三大核心问题
在并发编程中,通常需要解决以下三个问题:
- 原子性(Atomicity):操作不能被中断或分割,必须完整执行。
- 可见性(Visibility):一个线程对共享变量的修改,其他线程能够立即看到。
- 有序性(Ordering):程序执行顺序按照代码的先后顺序进行。
🌈 二、volatile
关键字详解
🔍 1. 作用:保证可见性,禁止指令重排
volatile
修饰字段(成员变量)时,表示:
- 对该变量的所有读写操作都直接从主内存中读取/写入,不会使用线程工作内存中的副本;
- 对该变量的写操作会立即刷新回主内存;
- 禁止 JVM 对其进行指令重排序优化(部分语义);
📌 示例代码
private volatile boolean flag = false;public void stop() {flag = true;
}public void run() {while (!flag) {// 如果没有volatile,可能一直在循环}
}
🧠 内存模型支持
在 Java 内存模型(JMM)中:
volatile
变量具备 可见性 和 有序性(写-读有序);- 它本质通过在字节码层面加入
lock
指令,刷新缓存行实现主内存同步;
⚠️ 不能保证原子性
volatile int count = 0;
count++; // 非原子性操作
以上语句会被分解为:读取 → 加 1 → 写入,多个线程同时执行可能出现丢失更新。
🔒 三、synchronized
关键字详解
🔍 1. 作用:保证原子性 + 可见性 + 排他性
synchronized
关键字可用于方法或代码块,表示在同一时刻只允许一个线程访问同步块/方法,实现互斥锁的语义。
它确保:
- 原子性:同步块内部操作不可被其他线程打断;
- 可见性:释放锁前,线程对共享变量的修改会刷新回主内存;
- 互斥性:同一时刻只能有一个线程持有锁;
📌 示例代码
public synchronized void increment() {count++;
}// 或者使用代码块
synchronized (this) {count++;
}
⚙️ 底层实现
synchronized
的底层依赖于 JVM 的 Monitor(监视器锁)机制:
- 编译后生成字节码指令
monitorenter
和monitorexit
; - 每个对象都有一个 Monitor,用于加锁和释放;
- JDK 1.6 后支持偏向锁、轻量级锁、自旋锁等锁优化;
🆚 四、volatile 与 synchronized 对比
特性 | volatile | synchronized |
---|---|---|
修饰范围 | 字段(变量) | 方法或代码块 |
可见性 | ✅ 保证 | ✅ 保证 |
原子性 | ❌ 不保证 | ✅ 保证 |
排他性 | ❌ 无 | ✅ 有 |
是否加锁 | ❌ 无锁机制 | ✅ 基于对象锁 |
性能开销 | 小(无阻塞) | 大(上下文切换) |
死锁风险 | 无 | 有(多锁交叉时可能) |
应用场景 | 状态标志、配置同步等轻量用途 | 临界区保护、计数器等需要原子性 |
🎯 五、使用场景推荐
✅ 使用 volatile
的典型场景:
- 状态变量、布尔标志控制:
volatile boolean stopRequested = false;
- 双重检查锁单例模式:
private static volatile Singleton instance;public static Singleton getInstance() {if (instance == null) {synchronized(Singleton.class) {if (instance == null) {instance = new Singleton();}}}return instance;
}
⚠️ 没有
volatile
,由于指令重排序,可能导致获取到未初始化完成的对象。
✅ 使用 synchronized
的典型场景:
- 多线程操作共享资源,如计数器、列表等:
synchronized (list) {list.add("item");
}
- 实现线程安全的懒汉式单例:
public static synchronized Singleton getInstance() {if (instance == null) {instance = new Singleton();}return instance;
}
- 同步访问复杂逻辑,避免并发执行:
public synchronized void update() {// 执行多个原子性操作
}
📌 六、总结一句话
volatile
适合用于保证可见性但不涉及复合操作的场景;synchronized
适合用于需要原子性和互斥访问的逻辑块;
二者在某些场景可以配合使用,例如在**双检锁模式(DCL)**中,volatile
保证有序性与可见性,synchronized
保证创建过程的原子性。
✅ 最佳实践小贴士
建议 | 描述 |
---|---|
✅ 优先用原子类 | 如 AtomicInteger 替代 volatile + count++ |
✅ 合理划分锁粒度 | 使用细粒度锁提升并发性能 |
❌ 不要滥用 volatile | 它不能替代锁,只适合状态控制 |
✅ JDK8+ 推荐使用 Lock | 更灵活,支持可中断、限时、公平锁等 |
✅ 一句话总述(先答重点)
在 Java 中,
volatile
关键字主要保证变量的可见性和禁止指令重排序,而synchronized
则保证原子性、可见性和互斥性,通常用于保护临界区。
🧠 面试回答结构建议(STAR法)
✨ 1. 先解释两者基本含义
volatile:
- 用于修饰字段,保证多线程环境下的可见性;
- 当一个线程修改了
volatile
变量,会立刻刷新到主内存,其他线程读取的是最新值; - 同时禁止某些指令重排序,但不保证原子性;
synchronized:
- 可修饰方法或代码块,底层依赖 对象监视器锁(Monitor);
- 保证线程间的互斥执行(排他性);
- 同时具有原子性 + 可见性;
- 释放锁时会强制刷新工作内存,保证变量最新值对其他线程可见;
🔍 2. 举个例子说明 volatile 使用场景
比如我们定义了一个
volatile boolean flag
作为线程停止标志,一个线程写入true
,另一个线程能立刻感知停止条件;如果不加volatile
,由于线程缓存,另一个线程可能一直感知不到。
💥 3. 举个例子说明为什么 volatile 不够
如果我定义了
volatile int count = 0
,多个线程执行count++
,它不是原子操作,会导致竞态条件,即便是 volatile 修饰也无法避免数据错误。
🆚 4. 比较总结(用表述)
特性 | volatile | synchronized |
---|---|---|
可见性 | ✅ 保证 | ✅ 保证 |
原子性 | ❌ 不保证 | ✅ 保证 |
是否加锁 | ❌ 无锁 | ✅ 加锁(互斥) |
性能 | 高(但功能弱) | 低(但功能全) |
死锁风险 | 无 | 有可能(使用不当) |
🎯 5. 面试总结推荐语句
因此我在实际项目中会根据场景选用,比如只需要保证状态变量可见性时用
volatile
,需要修改共享资源或控制临界区时我会使用synchronized
或更高级的锁机制,如ReentrantLock
。
🧑💻 BONUS:面试官追问常见问题准备
Q1:volatile 可以替代 synchronized 吗?
答:不能。volatile 不保证原子性,不能用来保护临界区,仅适用于状态标志、单例 DCL 模式等轻量级同步场景。
Q2:你知道 volatile 背后的内存屏障吗?
答:是的。JVM 会在写 volatile 变量时插入 Store Barrier,在读时插入 Load Barrier,用于阻止指令重排,并强制刷新主内存。
Q3:如果让你优化 synchronized 性能,你会怎么做?
答:
- 使用细粒度锁代替粗粒度锁;
- 用
ReentrantLock
替代synchronized
,支持 tryLock、可中断等; - 利用并发容器(如
ConcurrentHashMap
)代替手动加锁; - 使用锁分段、CAS 原子类等替代加锁方式;