Java 并发大坑:volatile、synchronized、Lock 三者如何选择?

Java 并发大坑:volatile、synchronized、Lock 三者如何选择?

很多 Java 程序员对 volatilesynchronizedLock 的理解停留在"背八股"层面:

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++ 实际是三步:

  1. 读取 count
  2. count + 1
  3. 写回 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 管"精细控制"。

能不用锁就不用锁,能用简单锁就不用复杂锁。

相关推荐
gis开发之家9 小时前
Spring Boot 4 深度解析——参数接收大全:@RequestParam、@PathVariable、@RequestBody
java·spring boot·后端·spring
NutShell Wang9 小时前
Rust 1.97 实战迁移:v0 符号重整、Cargo 警告治理与位运算新 API
人工智能·后端·性能优化·rust·vibe coding
阑梦清川10 小时前
零成本把笔记转成双人播客:WorkBuddy + 腾讯云 TTS 完整教程
后端
赫媒派10 小时前
Go 1.27 来了:泛型方法补齐,JSON 提速不踩坑
后端·go·敏捷开发
不ok哥男人10 小时前
C# Roslyn 编译器平台实战:源生成器、分析器与代码修补
后端
长栎10 小时前
99% 的人把桥接模式当策略模式用——抽象与实现分离,你做的不是同一件事
后端
长栎10 小时前
你激活了 sharp-skills 一个模块,但你的项目从来不是单点活儿
后端
Lcos10 小时前
Kubernetes Toleration 六种写法详解:从精确匹配到全部放行
后端
元界metalite10 小时前
MyBatis事务只能靠Transactional吗-MetaLite为何只保留编程式事务
后端
用户94791358116210 小时前
20260821_090153_LangGraph_生产落地的_5_个关键坑:从_State
后端