Java 并发大坑:volatile、synchronized、Lock 三者如何选择?
很多 Java 程序员对volatile、synchronized和Lock的理解停留在“背八股”层面:
volatile 保证可见性,synchronized 重量级,Lock 更灵活。
但真正写并发代码时,选错一个,线上就是事故。本文从原理 → 适用场景 → 典型坑点 → 选择决策表,一次性讲清楚三者的正确打开方式。
一、先给结论(速查版)
场景 | 推荐 |
|---|---|
单纯状态标志(如停止线程) | ✅ |
复合操作(i++、check-then-act) | ❌ |
单线程写、多线程读 | ✅ |
临界区保护、简单互斥 | ✅ |
需要公平锁、可中断、超时、多条件队列 | ✅ |
高并发 + 低竞争 | ✅ |
高并发 + 高竞争 + 复杂控制 | ✅ |
👉一句话原则:
能用
volatile就别用锁;能用synchronized就别用Lock。
二、volatile:最容易被误用的关键字
1️⃣ volatile 到底解决了什么?
volatile 只解决两个问题:
✅可见性
✅禁止指令重排序
❌不保证原子性
// 错误示例 private volatile int count = 0; public void increment() { count++; // 非原子操作! }count++实际是三步:
读取 count
count + 1
写回 count
多线程下必然出问题。
2️⃣ volatile 的正确使用姿势
✅ 场景一:状态标志(最常见 & 最正确)
class Worker implements Runnable { private volatile boolean running = true; public void stop() { running = false; } @Override public void run() { while (running) { // do work } } }✔ 一个线程写,多个线程读
✔ 无复合操作
✔ 完美匹配 volatile
✅ 场景二:双重检查锁定(DCL)
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; } }⚠️没有 volatile = 可能返回半初始化对象
3️⃣ volatile 的经典误区
❌ 误区 1:volatile可以替代锁
❌ 误区 2:volatile i++是线程安全的
❌ 误区 3:volatile一定比锁快(在竞争激烈时未必)
三、synchronized:被低估的王者
1️⃣ synchronized 做了什么?
synchronized保证:
✅ 原子性
✅ 可见性
✅ 有序性(临界区内)
本质:互斥 + 内存屏障
2️⃣ JVM 对 synchronized 的优化(非常重要)
很多人还停留在“synchronized 是重量级锁”的旧认知里。
现代 JVM(JDK 8+)已经实现了锁升级机制:
无锁 → 偏向锁 → 轻量级锁 → 重量级锁低竞争:几乎无开销
高竞争:自动升级,性能可控
👉在大多数业务系统中,synchronized 性能优于 Lock
3️⃣ synchronized 的最佳实践
public class Counter { private int count = 0; public synchronized void increment() { count++; } public synchronized int getCount() { return count; } }✔ 简单
✔ 安全
✔ 不易出错
4️⃣ synchronized 的局限性
❌ 无法响应中断
❌ 无法尝试获取锁(tryLock)
❌ 无法实现公平锁
❌ 条件队列只有一个(wait/notify)
当你需要这些能力时,才考虑Lock。
四、Lock:功能最强,但最危险
1️⃣ Lock 的核心优势
ReentrantLock lock = new ReentrantLock(); lock.lock(); try { // 临界区 } finally { lock.unlock(); }✅ 可中断(lockInterruptibly)
✅ 超时获取锁(tryLock(timeout))
✅ 公平锁
✅ 多条件变量(Condition)
2️⃣ Lock 的典型使用场景
✅ 场景一:可中断锁
lock.lockInterruptibly(); try { // 处理任务 } finally { lock.unlock(); }用于防止死锁、响应线程中断
✅ 场景二:多条件队列
Condition notFull = lock.newCondition(); Condition notEmpty = lock.newCondition();这是synchronized + wait/notify做不到的。
3️⃣ Lock 的大坑(90% 的人踩过)
❌忘记 unlock
lock.lock(); doSomething(); // 抛异常 → 锁永远不释放✅ 必须放在finally
❌重复加锁导致死锁
lock.lock(); lock.lock(); // 忘了解锁一次❌性能反而更差
在低竞争场景下,Lock的性能通常不如synchronized。
五、三者对比总结(面试 & 实战必看)
特性 | volatile | synchronized | Lock |
|---|---|---|---|
原子性 | ❌ | ✅ | ✅ |
可见性 | ✅ | ✅ | ✅ |
有序性 | 部分 | ✅ | ✅ |
可重入 | ❌ | ✅ | ✅ |
可中断 | ❌ | ❌ | ✅ |
公平锁 | ❌ | ❌ | ✅ |
多条件队列 | ❌ | ❌ | ✅ |
复杂度 | ⭐ | ⭐⭐ | ⭐⭐⭐⭐ |
推荐程度 | 慎用 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ |
六、选择决策流程图(建议收藏)
是否只是状态标志? ├─ 是 → volatile └─ 否 是否需要复杂锁控制(中断/超时/公平/多条件)? ├─ 是 → Lock └─ 否 → synchronized七、真实线上事故案例(警示)
💥 案例:volatile + i++ 导致库存超卖
private volatile int stock = 100; public void reduceStock() { stock--; }结果:
✈️ 库存扣成负数
💸 资损事故
原因:volatile 不保证原子性
✅ 正确做法:
synchronized (this) { stock--; }或
AtomicInteger stock = new AtomicInteger(100); stock.decrementAndGet();八、终极建议
并发编程的第一原则是:不要自己发明并发控制。
能用不可变对象就不用 volatile
能用
synchronized就不用Lock能用并发容器就不用手写锁
能用
AtomicXXX就不用synchronized
九、一句话总结
volatile 管“看见”,synchronized 管“互斥”,Lock 管“精细控制”。
能不用锁就不用锁,能用简单锁就不用复杂锁。