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